Ejemplo n.º 1
0
    def __init__(self):
        self.ioloop = ioloop.IOLoop.instance()

        self.prefs = UserPreferences()
        self.player = Player()
        self.recorder = Recorder()
        self.recordhandle = None

        self.screenshot_generator = ScreenshotGenerator()
        self.websockets = []

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok,bundlepath,title:None

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi  = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if not hmiOpened:
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)

        self.host = Host(self.hmi, self.prefs, self.msg_callback)
Ejemplo n.º 2
0
    def __init__(self):
        logging.basicConfig(level=(logging.DEBUG if LOG else logging.WARNING))

        self.prefs = UserPreferences()
        self.player = Player()
        self.recorder = Recorder()
        self.recordhandle = None

        self.screenshot_generator = ScreenshotGenerator()
        self.websockets = []

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok, bundlepath, title: None

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, HMI_TIMEOUT,
                           self.hmi_initialized_cb, self.hmi_reinit_cb)
            hmiOpened = self.hmi.sp is not None

        #print("Using HMI =>", hmiOpened)

        if not hmiOpened:
            self.hmi = FakeHMI(self.hmi_initialized_cb)
            print("Using FakeHMI =>", self.hmi)

        self.host = Host(self.hmi, self.prefs, self.msg_callback)
Ejemplo n.º 3
0
class Session(object):
    def __init__(self):
        self.ioloop = ioloop.IOLoop.instance()

        self.prefs = UserPreferences()
        self.player = Player()
        self.recorder = Recorder()
        self.recordhandle = None

        self.screenshot_generator = ScreenshotGenerator()
        self.websockets = []

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok,bundlepath,title:None

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi  = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if not hmiOpened:
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)

        self.host = Host(self.hmi, self.prefs, self.msg_callback)

    def signal_save(self):
        # reuse HMI function
        self.host.hmi_save_current_pedalboard(lambda r:None)

    def signal_device_updated(self):
        self.msg_callback("cc-device-updated")

    def signal_disconnect(self):
        sockets = self.websockets
        self.websockets = []
        for ws in sockets:
            ws.write_message("stop")
            ws.close()
        self.host.end_session(lambda r:None)

    def get_hardware_actuators(self):
        return self.host.addressings.get_actuators()

    # -----------------------------------------------------------------------------------------------------------------
    # App utilities, needed only for mod-app

    def setupApp(self, pedalboardChangedCallback):
        self.pedalboard_changed_callback = pedalboardChangedCallback

    def reconnectApp(self):
        if self.host.readsock is not None:
            self.host.readsock.close()
            self.host.readsock = None
        if self.host.writesock is not None:
            self.host.writesock.close()
            self.host.writesock = None
        self.host.open_connection_if_needed(None)

    # -----------------------------------------------------------------------------------------------------------------
    # Initialization

    @gen.coroutine
    def hmi_initialized_cb(self):
        logging.info("hmi initialized")
        self.hmi.initialized = True
        uiConnected = bool(len(self.websockets) > 0)
        yield gen.Task(self.host.initialize_hmi, uiConnected)

    # -----------------------------------------------------------------------------------------------------------------
    # Webserver callbacks, called from the browser (see webserver.py)
    # These will be called as a reponse to an action in the browser.
    # A callback must always be used unless specified otherwise.

    # Add a new plugin, starts enabled (ie, not bypassed)
    def web_add(self, instance, uri, x, y, callback):
        self.host.add_plugin(instance, uri, x, y, callback)

    # Remove a plugin
    def web_remove(self, instance, callback):
        self.host.remove_plugin(instance, callback)

    # Address a plugin parameter
    def web_parameter_address(self, port, actuator_uri, label, minimum, maximum, value, steps, callback):
        instance, portsymbol = port.rsplit("/",1)
        self.host.address(instance, portsymbol, actuator_uri, label, minimum, maximum, value, steps, callback)

    # Connect 2 ports
    def web_connect(self, port_from, port_to, callback):
        self.host.connect(port_from, port_to, callback)

    # Disconnect 2 ports
    def web_disconnect(self, port_from, port_to, callback):
        self.host.disconnect(port_from, port_to, callback)

    # Save the current pedalboard
    # returns saved bundle path
    def web_save_pedalboard(self, title, asNew):
        bundlepath = self.host.save(title, asNew)
        self.pedalboard_changed_callback(True, bundlepath, title)
        self.screenshot_generator.schedule_screenshot(bundlepath)
        return bundlepath

    # Get list of Hardware MIDI devices
    # returns (devsInUse, devList)
    def web_get_midi_device_list(self):
        return self.host.get_midi_ports()

    # Set the selected MIDI devices to @a newDevs
    # Will remove or add new JACK ports as needed
    def web_set_midi_devices(self, newDevs):
        return self.host.set_midi_devices(newDevs)

    # Send a ping to HMI and Websockets
    def web_ping(self, callback):
        if self.hmi.initialized:
            self.hmi.ping(callback)
        else:
            callback(False)
        self.msg_callback("ping")

    # A new webbrowser page has been open
    # We need to cache its socket address and send any msg callbacks to it
    def websocket_opened(self, ws, callback):
        def ready(ok):
            self.websockets.append(ws)
            self.host.open_connection_if_needed(ws)
            callback(True)

        # if this is the 1st socket, start ui session
        if len(self.websockets) == 0:
            self.host.start_session(ready)
        else:
            ready(True)

    # Webbrowser page closed
    def websocket_closed(self, ws, callback):
        try:
            self.websockets.remove(ws)
        except ValueError:
            pass

        # if this is the last socket, end ui session
        if len(self.websockets) == 0:
            self.host.end_session(callback)
        else:
            callback(True)

    # -----------------------------------------------------------------------------------------------------------------

    # Start recording
    def web_recording_start(self):
        self.player.stop()
        self.recorder.start()

    # Stop recording
    def web_recording_stop(self):
        if self.recordhandle is not None:
            self.recordhandle.close()
        self.recordhandle = self.recorder.stop(True)

    # Delete previous recording, if any
    def web_recording_delete(self):
        self.player.stop()

        if self.recordhandle is not None:
            self.recordhandle.close()
            self.recordhandle = None

    # Return recording data
    def web_recording_download(self):
        if self.recordhandle is None:
            return ""

        self.recordhandle.seek(0)
        return self.recordhandle.read()

    # Playback of previous recording started
    def web_playing_start(self, callback):
        if self.recordhandle is None:
            self.recordhandle = self.recorder.stop(True)

        def stop():
            self.host.unmute()
            callback()

        def schedule_stop():
            self.ioloop.add_timeout(timedelta(seconds=0.5), stop)

        self.host.mute()
        self.player.play(self.recordhandle, schedule_stop)

    # Playback stopped
    def web_playing_stop(self):
        self.player.stop()

    # -----------------------------------------------------------------------------------------------------------------
    # Websocket funtions, called when receiving messages from socket (see webserver.py)
    # There are no callbacks for these functions.

    # Set a plugin parameter
    # We use ":bypass" symbol for on/off state
    def ws_parameter_set(self, port, value, ws):
        instance, portsymbol = port.rsplit("/",1)

        if portsymbol == ":bypass":
            bvalue = value >= 0.5
            self.host.bypass(instance, bvalue, None)
        else:
            self.host.param_set(port, value, None)

        self.msg_callback_broadcast("param_set %s %s %f" % (instance, portsymbol, value), ws)

    # Set a plugin block position within the canvas
    def ws_plugin_position(self, instance, x, y):
        self.host.set_position(instance, x, y)

    # set the size of the pedalboard (in 1:1 view, aka "full zoom")
    def ws_pedalboard_size(self, width, height):
        self.host.set_pedalboard_size(width, height)

    # -----------------------------------------------------------------------------------------------------------------
    # TODO
    # Everything after this line is yet to be documented

    def msg_callback(self, msg):
        for ws in self.websockets:
            ws.write_message(msg)

    def msg_callback_broadcast(self, msg, ws2):
        for ws in self.websockets:
            if ws == ws2: continue
            ws.write_message(msg)

    def load_pedalboard(self, bundlepath, isDefault):
        title = self.host.load(bundlepath, isDefault)
        if isDefault:
            bundlepath = ""
            title = ""
        self.pedalboard_changed_callback(True, bundlepath, title)
        return title

    def reset(self, callback):
        def reset_host(ok):
            self.host.reset(callback)

        if self.hmi.initialized:
            def clear_hmi(ok):
                self.hmi.clear(reset_host)
            self.host.setNavigateWithFootswitches(False, clear_hmi)
        else:
            reset_host(True)

        self.pedalboard_changed_callback(True, "", "")

    # host commands

    def bypass(self, instance, value, callback):
        value = int(value) > 0
        self.host.enable(instance, value, callback)

    def format_port(self, port):
        if not 'system' in port and not 'effect' in port:
            port = "effect_%s" % port
        return port
Ejemplo n.º 4
0
    def __init__(self):
        self.host_initialized = False

        self._tuner = False
        self._tuner_port = 1
        self._peakmeter = False

        self.monitor_server = None
        self.current_bank = None
        self.hmi_initialized = False

        # JACK client name of the backend
        self.backend_client_name = "ingen"

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok, bundlepath, title: None

        # For saving the current pedalboard bundlepath and title
        self.bundlepath = None
        self.title = None

        self.engine_samplerate = 48000  # default value
        self.jack_client = None
        self.xrun_count = 0
        self.xrun_count2 = 0

        self.ioloop = ioloop.IOLoop.instance()

        socketpath = os.environ.get("MOD_INGEN_SOCKET_URI",
                                    "unix:///tmp/ingen.sock")
        if DEV_HOST:
            self.host = FakeHost(socketpath)
        else:
            self.host = Host(socketpath)

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE,
                           self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if hmiOpened:
            # If all ok, use addressings
            self.addressings = Addressing(self.hmi)
        else:
            # Otherwise disable HMI entirely
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE,
                               self.hmi_initialized_cb)
            self.addressings = None

        self.recorder = Recorder()
        self.player = Player()
        self.mute_state = True
        self.recording = None
        self.instances = []
        self.screenshot_generator = ScreenshotGenerator()

        self._clipmeter = Clipmeter(self.hmi)
        self.websockets = []
        self.mididevuuids = []

        self.jack_cpu_load_timer = ioloop.PeriodicCallback(
            self.jack_cpu_load_timer_callback, 1000)
        self.jack_xrun_timer = ioloop.PeriodicCallback(
            self.jack_xrun_timer_callback, 500)

        self.ioloop.add_callback(self.init_jack)
        self.ioloop.add_callback(self.init_socket)
Ejemplo n.º 5
0
class Session(object):
    def __init__(self):
        self.host_initialized = False

        self._tuner = False
        self._tuner_port = 1
        self._peakmeter = False

        self.monitor_server = None
        self.current_bank = None
        self.hmi_initialized = False

        # JACK client name of the backend
        self.backend_client_name = "ingen"

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok, bundlepath, title: None

        # For saving the current pedalboard bundlepath and title
        self.bundlepath = None
        self.title = None

        self.engine_samplerate = 48000  # default value
        self.jack_client = None
        self.xrun_count = 0
        self.xrun_count2 = 0

        self.ioloop = ioloop.IOLoop.instance()

        socketpath = os.environ.get("MOD_INGEN_SOCKET_URI",
                                    "unix:///tmp/ingen.sock")
        if DEV_HOST:
            self.host = FakeHost(socketpath)
        else:
            self.host = Host(socketpath)

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE,
                           self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if hmiOpened:
            # If all ok, use addressings
            self.addressings = Addressing(self.hmi)
        else:
            # Otherwise disable HMI entirely
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE,
                               self.hmi_initialized_cb)
            self.addressings = None

        self.recorder = Recorder()
        self.player = Player()
        self.mute_state = True
        self.recording = None
        self.instances = []
        self.screenshot_generator = ScreenshotGenerator()

        self._clipmeter = Clipmeter(self.hmi)
        self.websockets = []
        self.mididevuuids = []

        self.jack_cpu_load_timer = ioloop.PeriodicCallback(
            self.jack_cpu_load_timer_callback, 1000)
        self.jack_xrun_timer = ioloop.PeriodicCallback(
            self.jack_xrun_timer_callback, 500)

        self.ioloop.add_callback(self.init_jack)
        self.ioloop.add_callback(self.init_socket)

    def __del__(self):
        if self.jack_client is None:
            return
        jacklib.deactivate(self.jack_client)
        jacklib.client_close(self.jack_client)
        self.jack_client = None
        print("jacklib client deactivated")

    def get_hardware(self):
        if self.addressings is None:
            return {}
        hw = deepcopy(get_hardware())
        hw["addressings"] = self.addressings.get_addressings()
        return hw

    # -----------------------------------------------------------------------------------------------------------------
    # App utilities, needed only for mod-app

    def setupApp(self, clientName, pedalboardChangedCallback):
        self.backend_client_name = clientName
        self.pedalboard_changed_callback = pedalboardChangedCallback

    def reconnectApp(self):
        if self.host.sock is not None:
            self.host.sock.close()
            self.host.sock = None
        self.host.open_connection_if_needed(self.host_callback)

    # -----------------------------------------------------------------------------------------------------------------
    # Initialization

    def autoconnect_jack(self):
        if self.jack_client is None:
            return

        for i in range(1, INGEN_NUM_AUDIO_INS + 1):
            jacklib.connect(
                self.jack_client, "system:capture_%i" % i,
                "%s:audio_port_%i_in" % (self.backend_client_name, i))

        for i in range(1, INGEN_NUM_AUDIO_OUTS + 1):
            jacklib.connect(
                self.jack_client,
                "%s:audio_port_%i_out" % (self.backend_client_name, i),
                "system:playback_%i" % i)

        if not DEV_HMI:
            # this means we're using HMI, so very likely running MOD hardware
            jacklib.connect(self.jack_client, "alsa_midi:ttymidi MIDI out in",
                            "%s:midi_port_1_in" % self.backend_client_name)
            jacklib.connect(self.jack_client,
                            "%s:midi_port_1_out" % self.backend_client_name,
                            "alsa_midi:ttymidi MIDI in out")

    def init_jack(self):
        self.jack_client = jacklib.client_open(
            "%s-helper" % self.backend_client_name, jacklib.JackNoStartServer,
            None)
        self.xrun_count = 0
        self.xrun_count2 = 0

        if self.jack_client is None:
            return

        #jacklib.jack_set_port_registration_callback(self.jack_client, self.JackPortRegistrationCallback, None)
        jacklib.set_property_change_callback(self.jack_client,
                                             self.JackPropertyChangeCallback,
                                             None)
        #jacklib.set_xrun_callback(self.jack_client, self.JackXRunCallback, None)
        jacklib.on_shutdown(self.jack_client, self.JackShutdownCallback, None)
        jacklib.activate(self.jack_client)
        print("jacklib client activated")

        if INGEN_AUTOCONNECT:
            self.ioloop.add_timeout(timedelta(seconds=3.0),
                                    self.autoconnect_jack)

    def init_socket(self):
        self.host.open_connection_if_needed(self.host_callback)

    def hmi_initialized_cb(self):
        logging.info("hmi initialized")
        self.hmi_initialized = True
        self.hmi.clear()

        if self.addressings is not None:
            self.addressings.init_host()

    # -----------------------------------------------------------------------------------------------------------------
    # Timers (start and stop in sync with webserver IOLoop)

    def start_timers(self):
        self.jack_cpu_load_timer.start()
        self.jack_xrun_timer.start()

    def stop_timers(self):
        self.jack_xrun_timer.stop()
        self.jack_cpu_load_timer.stop()

    # -----------------------------------------------------------------------------------------------------------------
    # Webserver callbacks, called from the browser (see webserver.py)
    # These will be called as a reponse to an action in the browser.
    # A callback must always be used unless specified otherwise.

    # Add a new plugin, starts enabled (ie, not bypassed)
    def web_add(self, instance, uri, x, y, callback):
        self.host.add_plugin(instance, uri, True, x, y, callback)

    # Remove a plugin
    def web_remove(self, instance, callback):
        self.host.remove_plugin(instance, callback)

    # Set a plugin parameter
    # We use ":bypass" symbol for on/off state
    def web_parameter_set(self, port, value, callback):
        instance, port2 = port.rsplit("/", 1)

        if port2 == ":bypass":
            value = value >= 0.5
            self.host.enable(instance, not value, callback)
        else:
            self.host.param_set(port, value, callback)

        #self.recorder.parameter(port, value)

    # Address a plugin parameter
    def web_parameter_address(self, port, actuator_uri, label, maximum,
                              minimum, value, steps, callback):
        if self.addressings is None or not self.hmi_initialized:
            callback(False)
            return

        instance, port2 = port.rsplit("/", 1)
        self.addressings.address(instance, port2, actuator_uri, label, maximum,
                                 minimum, value, steps, callback)

    # Set a parameter for MIDI learn
    def web_parameter_midi_learn(self, port, callback):
        self.host.midi_learn(port, callback)

    # Load a plugin preset
    def web_preset_load(self, instance, uri, callback):
        self.host.preset_load(instance, uri, callback)

    # Set a plugin block position within the canvas
    def web_set_position(self, instance, x, y, callback):
        self.host.set_position(instance, x, y, callback)

    # Connect 2 ports
    def web_connect(self, port_from, port_to, callback):
        self.host.connect(port_from, port_to, callback)

    # Disconnect 2 ports
    def web_disconnect(self, port_from, port_to, callback):
        self.host.disconnect(port_from, port_to, callback)

    # Save the current pedalboard
    # The order of events is:
    #  1. set the graph name to 'title'
    #  2. ask backend to save
    #  3. create manifest.ttl
    #  4. create addressings.json file
    # Step 2 is asynchronous, so it happens while 3 and 4 are taking place.
    def web_save_pedalboard(self, title, asNew, callback):
        titlesym = symbolify(title)

        # Save over existing bundlepath
        if self.bundlepath and os.path.exists(
                self.bundlepath) and os.path.isdir(
                    self.bundlepath) and not asNew:
            bundlepath = self.bundlepath

        # Save new
        else:
            lv2path = os.path.expanduser("~/.lv2/")  # FIXME: cross-platform
            trypath = os.path.join(lv2path, "%s.pedalboard" % titlesym)

            # if trypath already exists, generate a random bundlepath based on title
            if os.path.exists(trypath):
                from random import randint

                while True:
                    trypath = os.path.join(
                        lv2path,
                        "%s-%i.pedalboard" % (titlesym, randint(1, 99999)))
                    if os.path.exists(trypath):
                        continue
                    bundlepath = trypath
                    break

            # trypath doesn't exist yet, use it
            else:
                bundlepath = trypath

                # just in case..
                if not os.path.exists(lv2path):
                    os.mkdir(lv2path)

            os.mkdir(bundlepath)

        # Various steps triggered by callbacks

        def step1(ok):
            if ok:
                self.host.save(os.path.join(bundlepath, "%s.ttl" % titlesym),
                               step2)
            else:
                callback(False)

        def step2(ok):
            if ok:
                self.bundlepath = bundlepath
                self.title = title
            else:
                self.bundlepath = None
                self.title = None

            self.pedalboard_changed_callback(ok, bundlepath, title)

            if not ok:
                callback(False)
                return

            # Create a custom manifest.ttl, not created by ingen because we want *.pedalboard extension
            with open(os.path.join(bundlepath, "manifest.ttl"), 'w') as fh:
                fh.write("""\
@prefix ingen: <http://drobilla.net/ns/ingen#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix pedal: <http://moddevices.com/ns/modpedal#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .

<%s.ttl>
    lv2:prototype ingen:GraphPrototype ;
    a lv2:Plugin ,
        ingen:Graph ,
        pedal:Pedalboard ;
    rdfs:seeAlso <%s.ttl> .
""" % (titlesym, titlesym))

            # Create a addressings.json file
            addressings = self.addressings.get_addressings(
            ) if self.addressings is not None else {}

            with open(os.path.join(bundlepath, "addressings.json"), 'w') as fh:
                json.dump(addressings, fh)

            # All ok!
            callback(True)

        # start here
        self.host.set_pedalboard_name(title, step1)

    # Get list of Hardware MIDI devices
    # returns (devsInUse, devList)
    def web_get_midi_device_list(self):
        return self.get_midi_ports(
            self.backend_client_name), self.get_midi_ports("alsa_midi")

    # Set the selected MIDI devices to @a newDevs
    # Will remove or add new JACK ports as needed
    @gen.engine
    def web_set_midi_devices(self, newDevs):
        curDevs = self.get_midi_ports(self.backend_client_name)

        # remove
        for dev in curDevs:
            if dev in newDevs:
                continue
            if dev.startswith("MIDI Port-"):
                continue
            dev, modes = dev.rsplit(" (", 1)
            jacklib.disconnect(self.jack_client, "alsa_midi:%s in" % dev,
                               self.backend_client_name + ":control_in")

            def remove_external_port_in(callback):
                self.host.remove_external_port(dev + " in")
                callback(True)

            def remove_external_port_out(callback):
                self.host.remove_external_port(dev + " out")
                callback(True)

            yield gen.Task(remove_external_port_in)

            if "out" in modes:
                yield gen.Task(remove_external_port_out)

        # add
        for dev in newDevs:
            if dev in curDevs:
                continue
            dev, modes = dev.rsplit(" (", 1)

            def add_external_port_in(callback):
                self.host.add_external_port(dev + " in", "Input", "MIDI")
                callback(True)

            def add_external_port_out(callback):
                self.host.add_external_port(dev + " out", "Output", "MIDI")
                callback(True)

            yield gen.Task(add_external_port_in)

            if "out" in modes:
                yield gen.Task(add_external_port_out)

    # Send a ping to HMI
    def web_ping_hmi(self, callback):
        self.hmi.ping(callback)

    # A new webbrowser page has been open
    # We need to cache its socket address and send any msg callbacks to it
    def websocket_opened(self, ws):
        self.websockets.append(ws)

        self.host.open_connection_if_needed(self.host_callback)
        self.host.get("/graph")

    # Webbrowser page closed
    def websocket_closed(self, ws):
        self.websockets.remove(ws)

    # -----------------------------------------------------------------------------------------------------------------
    # JACK callbacks, called by JACK itself
    # We must take care to ensure not to block for long periods of time or else audio will skip or xrun.

    # Callback for when a port appears or disappears
    # We use this to trigger a auto-connect mode
    #def JackPortRegistrationCallback(self, port, registered, arg):
    #if self.jack_client is None:
    #return
    #if not registered:
    #return

    # Callback for when a client or port property changes.
    # We use this to know the full length name of ingen created ports.
    def JackPropertyChangeCallback(self, subject, key, change, arg):
        if self.jack_client is None:
            return
        if change != jacklib.PropertyCreated:
            return
        if key != jacklib.bJACK_METADATA_PRETTY_NAME:
            return

        self.mididevuuids.append(subject)
        self.ioloop.add_callback(self.jack_midi_devs_callback)

    # Callback for when an xrun occurs
    def JackXRunCallback(self, arg):
        self.xrun_count += 1
        return 0

    # Callback for when JACK has shutdown or our client zombified
    def JackShutdownCallback(self, arg):
        self.jack_client = None

    # -----------------------------------------------------------------------------------------------------------------
    # Misc/Utility functions
    # We use these to save possibly duplicated code.

    # Get all available MIDI ports of a specific JACK client.
    def get_midi_ports(self, client_name):
        if self.jack_client is None:
            return []

        # get input and outputs separately
        in_ports = charPtrPtrToStringList(
            jacklib.get_ports(
                self.jack_client, client_name + ":",
                jacklib.JACK_DEFAULT_MIDI_TYPE,
                jacklib.JackPortIsPhysical | jacklib.JackPortIsOutput
                if client_name == "alsa_midi" else jacklib.JackPortIsInput))
        out_ports = charPtrPtrToStringList(
            jacklib.get_ports(
                self.jack_client, client_name + ":",
                jacklib.JACK_DEFAULT_MIDI_TYPE,
                jacklib.JackPortIsPhysical | jacklib.JackPortIsInput
                if client_name == "alsa_midi" else jacklib.JackPortIsOutput))

        if client_name != "alsa_midi":
            if "ingen:control_in" in in_ports:
                in_ports.remove("ingen:control_in")
            if "ingen:control_out" in out_ports:
                out_ports.remove("ingen:control_out")

            for i in range(len(in_ports)):
                uuid = jacklib.port_uuid(
                    jacklib.port_by_name(self.jack_client, in_ports[i]))
                ret, value, type_ = jacklib.get_property(
                    uuid, jacklib.JACK_METADATA_PRETTY_NAME)
                if ret == 0 and type_ == b"text/plain":
                    in_ports[i] = charPtrToString(value)

            for i in range(len(out_ports)):
                uuid = jacklib.port_uuid(
                    jacklib.port_by_name(self.jack_client, out_ports[i]))
                ret, value, type_ = jacklib.get_property(
                    uuid, jacklib.JACK_METADATA_PRETTY_NAME)
                if ret == 0 and type_ == b"text/plain":
                    out_ports[i] = charPtrToString(value)

        # remove suffixes from ports
        in_ports = [
            port.replace(client_name + ":", "", 1).rsplit(" in", 1)[0]
            for port in in_ports
        ]
        out_ports = [
            port.replace(client_name + ":", "", 1).rsplit(" out", 1)[0]
            for port in out_ports
        ]

        # add our own suffix now
        ports = []
        for port in in_ports:
            #if "Midi Through" in port:
            #continue
            if port in ("jackmidi", "OSS sequencer"):
                continue
            ports.append(port +
                         (" (in+out)" if port in out_ports else " (in)"))

        return ports

    # -----------------------------------------------------------------------------------------------------------------
    # Timer callbacks
    # These are functions called by the IO loop at regular intervals.

    # Single-shot callback that automatically connects new backend JACK MIDI ports to their hardware counterparts.
    def jack_midi_devs_callback(self):
        while len(self.mididevuuids) != 0:
            subject = self.mididevuuids.pop()

            ret, value, type_ = jacklib.get_property(
                subject, jacklib.JACK_METADATA_PRETTY_NAME)
            if ret != 0:
                continue
            if type_ != b"text/plain":
                continue

            value = charPtrToString(value)
            if not (value.endswith(" in") or value.endswith(" out")):
                continue

            mod_name = "%s:%s" % (self.backend_client_name,
                                  value.replace(" ", "_").replace("-",
                                                                  "_").lower())
            midi_name = "alsa_midi:%s" % value

            # All good, make connection now
            if value.endswith(" in"):
                jacklib.connect(self.jack_client, midi_name, mod_name)
                jacklib.connect(self.jack_client, midi_name,
                                self.backend_client_name + ":control_in")
            else:
                jacklib.connect(self.jack_client, mod_name, midi_name)

    # Callback for getting the current JACK cpu load and report it to the browser side.
    def jack_cpu_load_timer_callback(self):
        if self.jack_client is not None:
            msg = """[]
            a <http://lv2plug.in/ns/ext/patch#Set> ;
            <http://lv2plug.in/ns/ext/patch#subject> </engine/> ;
            <http://lv2plug.in/ns/ext/patch#property> <http://moddevices/ns/modpedal#cpuload> ;
            <http://lv2plug.in/ns/ext/patch#value> "%.1f" .
            """ % jacklib.cpu_load(self.jack_client)

            for ws in self.websockets:
                ws.write_message(msg)

    # Callback that checks if xruns have occured.
    def jack_xrun_timer_callback(self):
        for i in range(self.xrun_count2, self.xrun_count):
            self.xrun_count2 += 1
            #self.hmi.xrun()

    # -----------------------------------------------------------------------------------------------------------------
    # TODO
    # Everything after this line is yet to be documented

    @gen.engine
    def host_callback(self):
        if self.host_initialized:
            return

        self.host_initialized = True

        def msg_callback(msg):
            for ws in self.websockets:
                ws.write_message(msg)

        def saved_callback(bundlepath):
            if add_bundle_to_lilv_world(bundlepath):
                pass  #self.host.add_bundle(bundlepath)
            self.screenshot_generator.schedule_screenshot(bundlepath)

        def samplerate_callback(srate):
            self.engine_samplerate = srate

        def plugin_added_callback(instance, uri, enabled, x, y):
            if instance not in self.instances:
                self.instances.append(instance)

        def plugin_removed_callback(instance):
            if instance in self.instances:
                self.instances.remove(instance)

        self.host.msg_callback = msg_callback
        self.host.saved_callback = saved_callback
        self.host.samplerate_callback = samplerate_callback
        self.host.plugin_added_callback = plugin_added_callback
        self.host.plugin_removed_callback = plugin_removed_callback

        yield gen.Task(self.host.initial_setup)

        # Add ports
        for i in range(1, INGEN_NUM_AUDIO_INS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "Audio Port-%i in" % i, "Input", "Audio", callback))

        for i in range(1, INGEN_NUM_AUDIO_OUTS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "Audio Port-%i out" % i, "Output", "Audio", callback))

        for i in range(1, INGEN_NUM_MIDI_INS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "MIDI Port-%i in" % i, "Input", "MIDI", callback))

        for i in range(1, INGEN_NUM_MIDI_OUTS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "MIDI Port-%i out" % i, "Output", "MIDI", callback))

        for i in range(1, INGEN_NUM_CV_INS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "CV Port-%i in" % i, "Input", "CV", callback))

        for i in range(1, INGEN_NUM_CV_OUTS + 1):
            yield gen.Task(lambda callback: self.host.add_external_port(
                "CV Port-%i out" % i, "Output", "CV", callback))

    def load_pedalboard(self, bundlepath, title):
        self.bundlepath = bundlepath
        self.title = title
        self.host.load(bundlepath)
        self.pedalboard_changed_callback(True, bundlepath, title)

    def reset(self, callback):
        self.bundlepath = None
        self.title = None

        # Callback from socket
        def remove_next_plugin(ok):
            if not ok:
                callback(False)
                return

            try:
                instance = self.instances.pop(0)
            except IndexError:
                callback(True)
                return

            self.host.remove_plugin(instance, remove_next_plugin)

        # Callback from HMI, ignore ok status
        def remove_all_plugins(ok):
            remove_next_plugin(True)

        # Reset addressing data
        if self.addressings is not None:
            self.addressings.clear()

        # Wait for HMI if available
        if self.hmi_initialized:
            self.hmi.clear(remove_all_plugins)
        else:
            remove_next_plugin(True)

        self.pedalboard_changed_callback(True, "", "")

    #def setup_monitor(self):
    #if self.monitor_server is None:
    #from mod.monitor import MonitorServer
    #self.monitor_server = MonitorServer()
    #self.monitor_server.listen(12345)

    #self.set_monitor("localhost", 12345, 1, self.add_tools)

    #def add_tools(self, resp):
    #if resp:
    #self.add(CLIPMETER_URI, CLIPMETER_IN, self.setup_clipmeter_in, True)
    #self.add(CLIPMETER_URI, CLIPMETER_OUT, self.setup_clipmeter_out, True)

    #def setup_clipmeter_in(self, resp):
    #if resp:
    #self.connect("system:capture_1", "effect_%d:%s" % (CLIPMETER_IN, CLIPMETER_L), lambda r:None, True)
    #self.connect("system:capture_2", "effect_%d:%s" % (CLIPMETER_IN, CLIPMETER_R), lambda r:None, True)
    #self.parameter_monitor(CLIPMETER_IN, CLIPMETER_MON_L, ">=", 0, lambda r:None)
    #self.parameter_monitor(CLIPMETER_IN, CLIPMETER_MON_R, ">=", 0, lambda r:None)

    #def setup_clipmeter_out(self, resp):
    #if resp:
    #self.parameter_monitor(CLIPMETER_OUT, CLIPMETER_MON_L, ">=", 0, lambda r:None)
    #self.parameter_monitor(CLIPMETER_OUT, CLIPMETER_MON_R, ">=", 0, lambda r:None)

    # host commands

    def bypass(self, instance, value, callback):
        value = int(value) > 0
        #if not loaded:
        #    self._pedalboard.bypass(instance_id, value)
        #self.recorder.bypass(instance, value)
        self.host.enable(instance, value, callback)

    def format_port(self, port):
        if not 'system' in port and not 'effect' in port:
            port = "effect_%s" % port
        return port

    #def set_monitor(self, addr, port, status, callback):
    #self.host.monitor(addr, port, status, callback)

    #def parameter_monitor(self, instance_id, port_id, op, value, callback):
    #self.host.param_monitor(instance_id, port_id, op, value, callback)

    # END host commands

    # hmi commands
    def start_session(self, callback=None):
        def verify(resp):
            if callback:
                callback(resp)
            else:
                assert (resp)

        self.bank_address(0, 0, 1, 0, 0, lambda r: None)
        self.bank_address(0, 0, 1, 1, 0, lambda r: None)
        self.bank_address(0, 0, 1, 2, 0, lambda r: None)
        self.bank_address(0, 0, 1, 3, 0, lambda r: None)

        self.hmi.ui_con(verify)

    def end_session(self, callback):
        self.hmi.ui_dis(callback)

    def bank_address(self, hardware_type, hardware_id, actuator_type,
                     actuator_id, function, callback):
        """
        Function is an integer, meaning:
         - 0: Nothing (unaddress)
         - 1: True bypass
         - 2: Pedalboard up
         - 3: Pedalboard down
        """
        self.hmi.bank_config(hardware_type, hardware_id, actuator_type,
                             actuator_id, function, callback)

    def pedalboard_size(self, width, height, callback):
        self.host.set_pedalboard_size(width, height, callback)

    def clipmeter(self, pos, value):
        self._clipmeter.set(pos, value)

    def peakmeter(self, pos, value, peak, callback=None):
        cb = callback
        if not cb:
            cb = lambda r: r
        self.hmi.peakmeter(pos, value, peak, cb)

    def tuner(self, value, callback=None):
        cb = callback
        if not cb:
            cb = lambda r: r

        freq, note, cents = find_freqnotecents(value)
        self.hmi.tuner(freq, note, cents, cb)

    def start_recording(self):
        if self.player.playing:
            self.player.stop()
        self.recorder.start(self.backend_client_name)

    def stop_recording(self):
        if self.recorder.recording:
            self.recording = self.recorder.stop()
            return self.recording

    def start_playing(self, stop_callback):
        if self.recorder.recording:
            self.recording = self.recorder.stop()

        def stop():
            self.unmute(stop_callback)

        def schedule_stop():
            self.ioloop.add_timeout(timedelta(seconds=0.5), stop)

        def play():
            self.player.play(self.recording['handle'], schedule_stop)

        self.mute(play)

    def stop_playing(self):
        self.player.stop()

    def reset_recording(self):
        self.recording = None

    def mute(self, callback):
        return
        #self.set_audio_state(False, callback)

    def unmute(self, callback):
        return
Ejemplo n.º 6
0
    def __init__(self):
        self.host_initialized = False

        self._tuner = False
        self._tuner_port = 1
        self._peakmeter = False

        self.monitor_server = None
        self.current_bank = None
        self.hmi_initialized = False

        # JACK client name of the backend
        self.backend_client_name = "ingen"

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok,bundlepath,title:None

        # For saving the current pedalboard bundlepath and title
        self.bundlepath = None
        self.title      = None

        self.engine_samplerate = 48000 # default value
        self.jack_client = None
        self.xrun_count = 0
        self.xrun_count2 = 0

        self.ioloop = ioloop.IOLoop.instance()

        socketpath = os.environ.get("MOD_INGEN_SOCKET_URI", "unix:///tmp/ingen.sock")
        if DEV_HOST:
            self.host = FakeHost(socketpath)
        else:
            self.host = Host(socketpath)

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi  = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if hmiOpened:
            # If all ok, use addressings
            self.addressings = Addressing(self.hmi)
        else:
            # Otherwise disable HMI entirely
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            self.addressings = None

        self.recorder = Recorder()
        self.player = Player()
        self.mute_state = True
        self.recording = None
        self.instances = []
        self.screenshot_generator = ScreenshotGenerator()

        self._clipmeter = Clipmeter(self.hmi)
        self.websockets = []
        self.mididevuuids = []

        self.jack_cpu_load_timer = ioloop.PeriodicCallback(self.jack_cpu_load_timer_callback, 1000)
        self.jack_xrun_timer     = ioloop.PeriodicCallback(self.jack_xrun_timer_callback, 500)

        self.ioloop.add_callback(self.init_jack)
        self.ioloop.add_callback(self.init_socket)
Ejemplo n.º 7
0
class Session(object):
    def __init__(self):
        self.host_initialized = False

        self._tuner = False
        self._tuner_port = 1
        self._peakmeter = False

        self.monitor_server = None
        self.current_bank = None
        self.hmi_initialized = False

        # JACK client name of the backend
        self.backend_client_name = "ingen"

        # Used in mod-app to know when the current pedalboard changed
        self.pedalboard_changed_callback = lambda ok,bundlepath,title:None

        # For saving the current pedalboard bundlepath and title
        self.bundlepath = None
        self.title      = None

        self.engine_samplerate = 48000 # default value
        self.jack_client = None
        self.xrun_count = 0
        self.xrun_count2 = 0

        self.ioloop = ioloop.IOLoop.instance()

        socketpath = os.environ.get("MOD_INGEN_SOCKET_URI", "unix:///tmp/ingen.sock")
        if DEV_HOST:
            self.host = FakeHost(socketpath)
        else:
            self.host = Host(socketpath)

        # Try to open real HMI
        hmiOpened = False

        if not DEV_HMI:
            self.hmi  = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            hmiOpened = self.hmi.sp is not None

        print("Using HMI =>", hmiOpened)

        if hmiOpened:
            # If all ok, use addressings
            self.addressings = Addressing(self.hmi)
        else:
            # Otherwise disable HMI entirely
            self.hmi = FakeHMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_initialized_cb)
            self.addressings = None

        self.recorder = Recorder()
        self.player = Player()
        self.mute_state = True
        self.recording = None
        self.instances = []
        self.screenshot_generator = ScreenshotGenerator()

        self._clipmeter = Clipmeter(self.hmi)
        self.websockets = []
        self.mididevuuids = []

        self.jack_cpu_load_timer = ioloop.PeriodicCallback(self.jack_cpu_load_timer_callback, 1000)
        self.jack_xrun_timer     = ioloop.PeriodicCallback(self.jack_xrun_timer_callback, 500)

        self.ioloop.add_callback(self.init_jack)
        self.ioloop.add_callback(self.init_socket)

    def __del__(self):
        if self.jack_client is None:
            return
        jacklib.deactivate(self.jack_client)
        jacklib.client_close(self.jack_client)
        self.jack_client = None
        print("jacklib client deactivated")

    def get_hardware(self):
        if self.addressings is None:
            return {}
        hw = deepcopy(get_hardware())
        hw["addressings"] = self.addressings.get_addressings()
        return hw

    # -----------------------------------------------------------------------------------------------------------------
    # App utilities, needed only for mod-app

    def setupApp(self, clientName, pedalboardChangedCallback):
        self.backend_client_name = clientName
        self.pedalboard_changed_callback = pedalboardChangedCallback

    def reconnectApp(self):
        if self.host.sock is not None:
            self.host.sock.close()
            self.host.sock = None
        self.host.open_connection_if_needed(self.host_callback)

    # -----------------------------------------------------------------------------------------------------------------
    # Initialization

    def autoconnect_jack(self):
        if self.jack_client is None:
            return

        for i in range(1, INGEN_NUM_AUDIO_INS+1):
            jacklib.connect(self.jack_client, "system:capture_%i" % i, "%s:audio_port_%i_in" % (self.backend_client_name, i))

        for i in range(1, INGEN_NUM_AUDIO_OUTS+1):
            jacklib.connect(self.jack_client,"%s:audio_port_%i_out" % (self.backend_client_name, i), "system:playback_%i" % i)

        if not DEV_HMI:
            # this means we're using HMI, so very likely running MOD hardware
            jacklib.connect(self.jack_client, "alsa_midi:ttymidi MIDI out in", "%s:midi_port_1_in" % self.backend_client_name)
            jacklib.connect(self.jack_client, "%s:midi_port_1_out" % self.backend_client_name, "alsa_midi:ttymidi MIDI in out")

    def init_jack(self):
        self.jack_client = jacklib.client_open("%s-helper" % self.backend_client_name, jacklib.JackNoStartServer, None)
        self.xrun_count  = 0
        self.xrun_count2 = 0

        if self.jack_client is None:
            return

        #jacklib.jack_set_port_registration_callback(self.jack_client, self.JackPortRegistrationCallback, None)
        jacklib.set_property_change_callback(self.jack_client, self.JackPropertyChangeCallback, None)
        #jacklib.set_xrun_callback(self.jack_client, self.JackXRunCallback, None)
        jacklib.on_shutdown(self.jack_client, self.JackShutdownCallback, None)
        jacklib.activate(self.jack_client)
        print("jacklib client activated")

        if INGEN_AUTOCONNECT:
            self.ioloop.add_timeout(timedelta(seconds=3.0), self.autoconnect_jack)

    def init_socket(self):
        self.host.open_connection_if_needed(self.host_callback)

    def hmi_initialized_cb(self):
        logging.info("hmi initialized")
        self.hmi_initialized = True
        self.hmi.clear()

        if self.addressings is not None:
            self.addressings.init_host()

    # -----------------------------------------------------------------------------------------------------------------
    # Timers (start and stop in sync with webserver IOLoop)

    def start_timers(self):
        self.jack_cpu_load_timer.start()
        self.jack_xrun_timer.start()

    def stop_timers(self):
        self.jack_xrun_timer.stop()
        self.jack_cpu_load_timer.stop()

    # -----------------------------------------------------------------------------------------------------------------
    # Webserver callbacks, called from the browser (see webserver.py)
    # These will be called as a reponse to an action in the browser.
    # A callback must always be used unless specified otherwise.

    # Add a new plugin, starts enabled (ie, not bypassed)
    def web_add(self, instance, uri, x, y, callback):
        self.host.add_plugin(instance, uri, True, x, y, callback)

    # Remove a plugin
    def web_remove(self, instance, callback):
        self.host.remove_plugin(instance, callback)

    # Set a plugin parameter
    # We use ":bypass" symbol for on/off state
    def web_parameter_set(self, port, value, callback):
        instance, port2 = port.rsplit("/",1)

        if port2 == ":bypass":
            value = value >= 0.5
            self.host.enable(instance, not value, callback)
        else:
            self.host.param_set(port, value, callback)

        #self.recorder.parameter(port, value)

    # Address a plugin parameter
    def web_parameter_address(self, port, actuator_uri, label, maximum, minimum, value, steps, callback):
        if self.addressings is None or not self.hmi_initialized:
            callback(False)
            return

        instance, port2 = port.rsplit("/",1)
        self.addressings.address(instance, port2, actuator_uri, label, maximum, minimum, value, steps, callback)

    # Set a parameter for MIDI learn
    def web_parameter_midi_learn(self, port, callback):
        self.host.midi_learn(port, callback)

    # Load a plugin preset
    def web_preset_load(self, instance, uri, callback):
        self.host.preset_load(instance, uri, callback)

    # Set a plugin block position within the canvas
    def web_set_position(self, instance, x, y, callback):
        self.host.set_position(instance, x, y, callback)

    # Connect 2 ports
    def web_connect(self, port_from, port_to, callback):
        self.host.connect(port_from, port_to, callback)

    # Disconnect 2 ports
    def web_disconnect(self, port_from, port_to, callback):
        self.host.disconnect(port_from, port_to, callback)

    # Save the current pedalboard
    # The order of events is:
    #  1. set the graph name to 'title'
    #  2. ask backend to save
    #  3. create manifest.ttl
    #  4. create addressings.json file
    # Step 2 is asynchronous, so it happens while 3 and 4 are taking place.
    def web_save_pedalboard(self, title, asNew, callback):
        titlesym = symbolify(title)

        # Save over existing bundlepath
        if self.bundlepath and os.path.exists(self.bundlepath) and os.path.isdir(self.bundlepath) and not asNew:
            bundlepath = self.bundlepath

        # Save new
        else:
            lv2path = os.path.expanduser("~/.lv2/") # FIXME: cross-platform
            trypath = os.path.join(lv2path, "%s.pedalboard" % titlesym)

            # if trypath already exists, generate a random bundlepath based on title
            if os.path.exists(trypath):
                from random import randint

                while True:
                    trypath = os.path.join(lv2path, "%s-%i.pedalboard" % (titlesym, randint(1,99999)))
                    if os.path.exists(trypath):
                        continue
                    bundlepath = trypath
                    break

            # trypath doesn't exist yet, use it
            else:
                bundlepath = trypath

                # just in case..
                if not os.path.exists(lv2path):
                    os.mkdir(lv2path)

            os.mkdir(bundlepath)

        # Various steps triggered by callbacks
        def step1(ok):
            if ok: self.host.save(os.path.join(bundlepath, "%s.ttl" % titlesym), step2)
            else: callback(False)

        def step2(ok):
            if ok:
                self.bundlepath = bundlepath
                self.title      = title
            else:
                self.bundlepath = None
                self.title      = None

            self.pedalboard_changed_callback(ok, bundlepath, title)

            if not ok:
                callback(False)
                return

            # Create a custom manifest.ttl, not created by ingen because we want *.pedalboard extension
            with open(os.path.join(bundlepath, "manifest.ttl"), 'w') as fh:
                fh.write("""\
@prefix ingen: <http://drobilla.net/ns/ingen#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix pedal: <http://moddevices.com/ns/modpedal#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .

<%s.ttl>
    lv2:prototype ingen:GraphPrototype ;
    a lv2:Plugin ,
        ingen:Graph ,
        pedal:Pedalboard ;
    rdfs:seeAlso <%s.ttl> .
""" % (titlesym, titlesym))

            # Create a addressings.json file
            addressings = self.addressings.get_addressings() if self.addressings is not None else {}

            with open(os.path.join(bundlepath, "addressings.json"), 'w') as fh:
                json.dump(addressings, fh)

            # All ok!
            callback(True)

        # start here
        self.host.set_pedalboard_name(title, step1)

    # Get list of Hardware MIDI devices
    # returns (devsInUse, devList)
    def web_get_midi_device_list(self):
        return self.get_midi_ports(self.backend_client_name), self.get_midi_ports("alsa_midi")

    # Set the selected MIDI devices to @a newDevs
    # Will remove or add new JACK ports as needed
    @gen.engine
    def web_set_midi_devices(self, newDevs):
        curDevs = self.get_midi_ports(self.backend_client_name)

        # remove
        for dev in curDevs:
            if dev in newDevs:
                continue
            if dev.startswith("MIDI Port-"):
                continue
            dev, modes = dev.rsplit(" (",1)
            jacklib.disconnect(self.jack_client, "alsa_midi:%s in" % dev, self.backend_client_name+":control_in")

            def remove_external_port_in(callback):
                self.host.remove_external_port(dev+" in")
                callback(True)
            def remove_external_port_out(callback):
                self.host.remove_external_port(dev+" out")
                callback(True)

            yield gen.Task(remove_external_port_in)

            if "out" in modes:
                yield gen.Task(remove_external_port_out)

        # add
        for dev in newDevs:
            if dev in curDevs:
                continue
            dev, modes = dev.rsplit(" (",1)

            def add_external_port_in(callback):
                self.host.add_external_port(dev+" in", "Input", "MIDI")
                callback(True)
            def add_external_port_out(callback):
                self.host.add_external_port(dev+" out", "Output", "MIDI")
                callback(True)

            yield gen.Task(add_external_port_in)

            if "out" in modes:
                yield gen.Task(add_external_port_out)

    # Send a ping to HMI
    def web_ping_hmi(self, callback):
        self.hmi.ping(callback)

    # A new webbrowser page has been open
    # We need to cache its socket address and send any msg callbacks to it
    def websocket_opened(self, ws):
        self.websockets.append(ws)

        self.host.open_connection_if_needed(self.host_callback)
        self.host.get("/graph")

    # Webbrowser page closed
    def websocket_closed(self, ws):
        self.websockets.remove(ws)

    # -----------------------------------------------------------------------------------------------------------------
    # JACK callbacks, called by JACK itself
    # We must take care to ensure not to block for long periods of time or else audio will skip or xrun.

    # Callback for when a port appears or disappears
    # We use this to trigger a auto-connect mode
    #def JackPortRegistrationCallback(self, port, registered, arg):
        #if self.jack_client is None:
            #return
        #if not registered:
            #return

    # Callback for when a client or port property changes.
    # We use this to know the full length name of ingen created ports.
    def JackPropertyChangeCallback(self, subject, key, change, arg):
        if self.jack_client is None:
            return
        if change != jacklib.PropertyCreated:
            return
        if key != jacklib.bJACK_METADATA_PRETTY_NAME:
            return

        self.mididevuuids.append(subject)
        self.ioloop.add_callback(self.jack_midi_devs_callback)

    # Callback for when an xrun occurs
    def JackXRunCallback(self, arg):
        self.xrun_count += 1
        return 0

    # Callback for when JACK has shutdown or our client zombified
    def JackShutdownCallback(self, arg):
        self.jack_client = None

    # -----------------------------------------------------------------------------------------------------------------
    # Misc/Utility functions
    # We use these to save possibly duplicated code.

    # Get all available MIDI ports of a specific JACK client.
    def get_midi_ports(self, client_name):
        if self.jack_client is None:
            return []

        # get input and outputs separately
        in_ports = charPtrPtrToStringList(jacklib.get_ports(self.jack_client, client_name+":", jacklib.JACK_DEFAULT_MIDI_TYPE,
                                                            jacklib.JackPortIsPhysical|jacklib.JackPortIsOutput
                                                            if client_name == "alsa_midi" else
                                                            jacklib.JackPortIsInput
                                                            ))
        out_ports = charPtrPtrToStringList(jacklib.get_ports(self.jack_client, client_name+":", jacklib.JACK_DEFAULT_MIDI_TYPE,
                                                             jacklib.JackPortIsPhysical|jacklib.JackPortIsInput
                                                             if client_name == "alsa_midi" else
                                                             jacklib.JackPortIsOutput
                                                             ))

        if client_name != "alsa_midi":
            if "ingen:control_in" in in_ports:
                in_ports.remove("ingen:control_in")
            if "ingen:control_out" in out_ports:
                out_ports.remove("ingen:control_out")

            for i in range(len(in_ports)):
                uuid = jacklib.port_uuid(jacklib.port_by_name(self.jack_client, in_ports[i]))
                ret, value, type_ = jacklib.get_property(uuid, jacklib.JACK_METADATA_PRETTY_NAME)
                if ret == 0 and type_ == b"text/plain":
                    in_ports[i] = charPtrToString(value)

            for i in range(len(out_ports)):
                uuid = jacklib.port_uuid(jacklib.port_by_name(self.jack_client, out_ports[i]))
                ret, value, type_ = jacklib.get_property(uuid, jacklib.JACK_METADATA_PRETTY_NAME)
                if ret == 0 and type_ == b"text/plain":
                    out_ports[i] = charPtrToString(value)

        # remove suffixes from ports
        in_ports  = [port.replace(client_name+":","",1).rsplit(" in" ,1)[0] for port in in_ports ]
        out_ports = [port.replace(client_name+":","",1).rsplit(" out",1)[0] for port in out_ports]

        # add our own suffix now
        ports = []
        for port in in_ports:
            #if "Midi Through" in port:
                #continue
            if port in ("jackmidi", "OSS sequencer"):
                continue
            ports.append(port + (" (in+out)" if port in out_ports else " (in)"))

        return ports

    # -----------------------------------------------------------------------------------------------------------------
    # Timer callbacks
    # These are functions called by the IO loop at regular intervals.

    # Single-shot callback that automatically connects new backend JACK MIDI ports to their hardware counterparts.
    def jack_midi_devs_callback(self):
        while len(self.mididevuuids) != 0:
            subject = self.mididevuuids.pop()

            ret, value, type_ = jacklib.get_property(subject, jacklib.JACK_METADATA_PRETTY_NAME)
            if ret != 0:
                continue
            if type_ != b"text/plain":
                continue

            value = charPtrToString(value)
            if not (value.endswith(" in") or value.endswith(" out")):
                continue

            mod_name  = "%s:%s" % (self.backend_client_name, value.replace(" ", "_").replace("-","_").lower())
            midi_name = "alsa_midi:%s" % value

            # All good, make connection now
            if value.endswith(" in"):
                jacklib.connect(self.jack_client, midi_name, mod_name)
                jacklib.connect(self.jack_client, midi_name, self.backend_client_name+":control_in")
            else:
                jacklib.connect(self.jack_client, mod_name, midi_name)

    # Callback for getting the current JACK cpu load and report it to the browser side.
    def jack_cpu_load_timer_callback(self):
        if self.jack_client is not None:
            msg = """[]
            a <http://lv2plug.in/ns/ext/patch#Set> ;
            <http://lv2plug.in/ns/ext/patch#subject> </engine/> ;
            <http://lv2plug.in/ns/ext/patch#property> <http://moddevices/ns/modpedal#cpuload> ;
            <http://lv2plug.in/ns/ext/patch#value> "%.1f" .
            """ % jacklib.cpu_load(self.jack_client)

            for ws in self.websockets:
                ws.write_message(msg)

    # Callback that checks if xruns have occured.
    def jack_xrun_timer_callback(self):
        for i in range(self.xrun_count2, self.xrun_count):
            self.xrun_count2 += 1
            #self.hmi.xrun()

    # -----------------------------------------------------------------------------------------------------------------
    # TODO
    # Everything after this line is yet to be documented

    @gen.engine
    def host_callback(self):
        if self.host_initialized:
            return

        self.host_initialized = True

        def msg_callback(msg):
            for ws in self.websockets:
                ws.write_message(msg)

        def saved_callback(bundlepath):
            if add_bundle_to_lilv_world(bundlepath):
                pass #self.host.add_bundle(bundlepath)
            self.screenshot_generator.schedule_screenshot(bundlepath)

        def samplerate_callback(srate):
            self.engine_samplerate = srate

        def plugin_added_callback(instance, uri, enabled, x, y):
            if instance not in self.instances:
                self.instances.append(instance)

        def plugin_removed_callback(instance):
            if instance in self.instances:
                self.instances.remove(instance)

        self.host.msg_callback = msg_callback
        self.host.saved_callback = saved_callback
        self.host.samplerate_callback = samplerate_callback
        self.host.plugin_added_callback = plugin_added_callback
        self.host.plugin_removed_callback = plugin_removed_callback

        yield gen.Task(self.host.initial_setup)

        # Add ports
        for i in range(1, INGEN_NUM_AUDIO_INS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("Audio Port-%i in" % i, "Input", "Audio", callback))

        for i in range(1, INGEN_NUM_AUDIO_OUTS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("Audio Port-%i out" % i, "Output", "Audio", callback))

        for i in range(1, INGEN_NUM_MIDI_INS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("MIDI Port-%i in" % i, "Input", "MIDI", callback))

        for i in range(1, INGEN_NUM_MIDI_OUTS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("MIDI Port-%i out" % i, "Output", "MIDI", callback))

        for i in range(1, INGEN_NUM_CV_INS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("CV Port-%i in" % i, "Input", "CV", callback))

        for i in range(1, INGEN_NUM_CV_OUTS+1):
            yield gen.Task(lambda callback: self.host.add_external_port("CV Port-%i out" % i, "Output", "CV", callback))

    def load_pedalboard(self, bundlepath, title):
        self.bundlepath = bundlepath
        self.title      = title
        self.host.load(bundlepath)
        self.pedalboard_changed_callback(True, bundlepath, title)

    def reset(self, callback):
        self.bundlepath = None
        self.title      = None

        # Callback from socket
        def remove_next_plugin(ok):
            if not ok:
                callback(False)
                return

            try:
                instance = self.instances.pop(0)
            except IndexError:
                callback(True)
                return

            self.host.remove_plugin(instance, remove_next_plugin)

        # Callback from HMI, ignore ok status
        def remove_all_plugins(ok):
            remove_next_plugin(True)

        # Reset addressing data
        if self.addressings is not None:
            self.addressings.clear()

        # Wait for HMI if available
        if self.hmi_initialized:
            self.hmi.clear(remove_all_plugins)
        else:
            remove_next_plugin(True)

        self.pedalboard_changed_callback(True, "", "")

    #def setup_monitor(self):
        #if self.monitor_server is None:
            #from mod.monitor import MonitorServer
            #self.monitor_server = MonitorServer()
            #self.monitor_server.listen(12345)

            #self.set_monitor("localhost", 12345, 1, self.add_tools)

    #def add_tools(self, resp):
        #if resp:
            #self.add(CLIPMETER_URI, CLIPMETER_IN, self.setup_clipmeter_in, True)
            #self.add(CLIPMETER_URI, CLIPMETER_OUT, self.setup_clipmeter_out, True)

    #def setup_clipmeter_in(self, resp):
        #if resp:
            #self.connect("system:capture_1", "effect_%d:%s" % (CLIPMETER_IN, CLIPMETER_L), lambda r:None, True)
            #self.connect("system:capture_2", "effect_%d:%s" % (CLIPMETER_IN, CLIPMETER_R), lambda r:None, True)
            #self.parameter_monitor(CLIPMETER_IN, CLIPMETER_MON_L, ">=", 0, lambda r:None)
            #self.parameter_monitor(CLIPMETER_IN, CLIPMETER_MON_R, ">=", 0, lambda r:None)

    #def setup_clipmeter_out(self, resp):
        #if resp:
            #self.parameter_monitor(CLIPMETER_OUT, CLIPMETER_MON_L, ">=", 0, lambda r:None)
            #self.parameter_monitor(CLIPMETER_OUT, CLIPMETER_MON_R, ">=", 0, lambda r:None)

    # host commands

    def bypass(self, instance, value, callback):
        value = int(value) > 0
        #if not loaded:
        #    self._pedalboard.bypass(instance_id, value)
        #self.recorder.bypass(instance, value)
        self.host.enable(instance, value, callback)

    def format_port(self, port):
        if not 'system' in port and not 'effect' in port:
            port = "effect_%s" % port
        return port

    #def set_monitor(self, addr, port, status, callback):
        #self.host.monitor(addr, port, status, callback)

    #def parameter_monitor(self, instance_id, port_id, op, value, callback):
        #self.host.param_monitor(instance_id, port_id, op, value, callback)

    # END host commands

    # hmi commands
    def start_session(self, callback=None):
        def verify(resp):
            if callback:
                callback(resp)
            else:
                assert(resp)
        self.bank_address(0, 0, 1, 0, 0, lambda r: None)
        self.bank_address(0, 0, 1, 1, 0, lambda r: None)
        self.bank_address(0, 0, 1, 2, 0, lambda r: None)
        self.bank_address(0, 0, 1, 3, 0, lambda r: None)

        self.hmi.ui_con(verify)

    def end_session(self, callback):
        self.hmi.ui_dis(callback)

    def bank_address(self, hardware_type, hardware_id, actuator_type, actuator_id, function, callback):
        """
        Function is an integer, meaning:
         - 0: Nothing (unaddress)
         - 1: True bypass
         - 2: Pedalboard up
         - 3: Pedalboard down
        """
        self.hmi.bank_config(hardware_type, hardware_id, actuator_type, actuator_id, function, callback)

    def pedalboard_size(self, width, height, callback):
        self.host.set_pedalboard_size(width, height, callback)

    def clipmeter(self, pos, value):
        self._clipmeter.set(pos, value)

    def peakmeter(self, pos, value, peak, callback=None):
        cb = callback
        if not cb:
            cb = lambda r: r
        self.hmi.peakmeter(pos, value, peak, cb)

    def tuner(self, value, callback=None):
        cb = callback
        if not cb:
            cb = lambda r: r

        freq, note, cents = find_freqnotecents(value)
        self.hmi.tuner(freq, note, cents, cb)

    def start_recording(self):
        if self.player.playing:
            self.player.stop()
        self.recorder.start(self.backend_client_name)

    def stop_recording(self):
        if self.recorder.recording:
            self.recording = self.recorder.stop()
            return self.recording

    def start_playing(self, stop_callback):
        if self.recorder.recording:
            self.recording = self.recorder.stop()
        def stop():
            self.unmute(stop_callback)
        def schedule_stop():
            self.ioloop.add_timeout(timedelta(seconds=0.5), stop)
        def play():
            self.player.play(self.recording['handle'], schedule_stop)
        self.mute(play)

    def stop_playing(self):
        self.player.stop()

    def reset_recording(self):
        self.recording = None

    def mute(self, callback):
        return
        #self.set_audio_state(False, callback)

    def unmute(self, callback):
        return