Ejemplo n.º 1
0
class Session(object):
    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)

    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()

    def wait_for_hardware_if_needed(self, callback):
        return self.host.addressings.wait_for_cc_if_needed(callback)

    # -----------------------------------------------------------------------------------------------------------------
    # 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):
        self.hmi.initialized = not self.hmi.isFake()
        uiConnected = bool(len(self.websockets) > 0)
        yield gen.Task(self.host.initialize_hmi, uiConnected)

    # This is very nasty, sorry
    def hmi_reinit_cb(self):
        if not os.path.exists("/usr/bin/hmi-reset"):
            return
        # stop websockets
        self.hmi.initialized = False
        self.signal_disconnect()
        # restart hmi
        os.system("/usr/bin/hmi-reset; /usr/bin/sleep 3")
        # reconnect to newly started hmi
        self.hmi = HMI(HMI_SERIAL_PORT, HMI_BAUD_RATE, HMI_TIMEOUT,
                       self.hmi_initialized_cb, self.hmi_reinit_cb)
        self.host.reconnect_hmi(self.hmi)

    # -----------------------------------------------------------------------------------------------------------------
    # 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, tempo, dividers, page,
                              subpage, coloured, momentary, operational_mode,
                              callback):
        instance, portsymbol = port.rsplit("/", 1)
        extras = {
            'tempo': tempo,
            'dividers': dividers,
            'page': page,
            'subpage': subpage,
            'coloured': coloured,
            'momentary': momentary,
            'operational_mode': operational_mode,
        }
        self.host.address(instance, portsymbol, actuator_uri, label, minimum,
                          maximum, value, steps, extras, callback)

    def web_set_sync_mode(self, mode, callback):
        self.host.set_sync_mode(mode, True, False, True, 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, callback):
        bundlepath, newPB = self.host.save(title, asNew, callback)
        self.pedalboard_changed_callback(True, bundlepath, title)

        if self.hmi.initialized and self.host.descriptor.get(
                'hmi_set_pb_name', False):
            self.hmi_set_pb_name(title)

        self.screenshot_generator.schedule_screenshot(bundlepath)
        return bundlepath, newPB

    # Get list of Hardware MIDI devices
    # returns (devsInUse, devList, names, midi_aggregated_mode)
    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, midi_aggregated_mode):
        return self.host.set_midi_devices(newDevs, midi_aggregated_mode)

    # 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")

    def web_cv_addressing_plugin_port_add(self, uri, name):
        return self.host.cv_addressing_plugin_port_add(uri, name)

    def web_cv_addressing_plugin_port_remove(self, uri, callback):
        self.host.cv_addressing_plugin_port_remove(uri, 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, callback):
        def ready(_):
            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():
            IOLoop.instance().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)

    # LV2 patch support
    def ws_patch_get(self, instance, uri, ws):
        self.host.patch_get(instance, uri, None)

    def ws_patch_set(self, instance, uri, valuetype, valuedata, ws):
        writable = self.host.patch_set(instance, uri, valuedata, None)
        self.msg_callback_broadcast(
            "patch_set %s %d %s %c %s" %
            (instance, 1 if writable else 0, uri, valuetype, valuedata), ws)

    # Set a plugin block position within the canvas
    def ws_plugin_position(self, instance, x, y, ws):
        self.host.set_position(instance, x, y)
        self.msg_callback_broadcast("plugin_pos %s %d %d" % (instance, x, y),
                                    ws)

    # 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)

    # -----------------------------------------------------------------------------------------------------------------
    # web session helpers

    @gen.coroutine
    def hmi_set_pb_name(self, name):
        yield gen.Task(self.hmi.set_pedalboard_name, name)

    @gen.coroutine
    def hmi_set_pb_and_ss_name(self, pbname):
        yield gen.Task(self.hmi.set_pedalboard_name, pbname)

        if self.host.descriptor.get('hmi_set_ss_name', False):
            ssname = self.host.snapshot_name() or DEFAULT_SNAPSHOT_NAME
            yield gen.Task(self.hmi.set_snapshot_name, ssname)

    # -----------------------------------------------------------------------------------------------------------------
    # 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):
        self.host.send_notmodified("feature_enable processing 0")
        title = self.host.load(bundlepath, isDefault)
        self.host.send_notmodified("feature_enable processing 1")
        if isDefault:
            bundlepath = ""
            title = ""

        if self.hmi.initialized and self.host.descriptor.get(
                'hmi_set_pb_name', False):
            self.hmi_set_pb_and_ss_name(title or UNTITLED_PEDALBOARD_NAME)

        self.pedalboard_changed_callback(True, bundlepath, title)
        return title

    def reset(self, callback):
        logging.debug("SESSION RESET")
        self.host.send_notmodified("feature_enable processing 0")

        def host_callback(resp):
            self.host.send_notmodified("feature_enable processing 1")
            callback(resp)

        def reset_host(_):
            self.host.reset(host_callback)

        if self.hmi.initialized:

            def set_pb_name(_):
                self.hmi.set_pedalboard_name(UNTITLED_PEDALBOARD_NAME,
                                             reset_host)

            def clear_ss_name(_):
                self.host.hmi_clear_ss_name(set_pb_name)

            def clear_hmi(_):
                self.hmi.clear(clear_ss_name)

            if self.host.descriptor.get("hmi_bank_navigation", False):
                self.host.setNavigateWithFootswitches(False, clear_hmi)
            else:
                clear_hmi(True)
        else:
            reset_host(True)

        # Update the title in HMI
        self.pedalboard_changed_callback(True, "", "")

    # host commands

    def format_port(self, port):
        if not 'system' in port and not 'effect' in port:
            port = "effect_%s" % port
        return port
Ejemplo n.º 2
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.º 3
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.º 4
0
class Session(object):
    def __init__(self):
        self.hmi_initialized = False
        self.host_initialized = False
        self.pedalboard_initialized = False

        self._playback_1_connected_ports = []
        self._playback_2_connected_ports = []
        self._tuner = False
        self._tuner_port = 1
        self._peakmeter = False

        self.monitor_server = None

        self.current_bank = None

        self.jack_bufsize = DEFAULT_JACK_BUFSIZE
        self.effect_index = EffectIndex()

        self._pedalboard = Pedalboard()

        #Protocol.register_cmd_callback("banks", self.hmi_list_banks)
        #Protocol.register_cmd_callback("pedalboards", self.hmi_list_pedalboards)
        Protocol.register_cmd_callback("hw_con", self.hardware_connected)
        Protocol.register_cmd_callback("hw_dis", self.hardware_disconnected)
        Protocol.register_cmd_callback("control_set", self.hmi_parameter_set)
        Protocol.register_cmd_callback("control_get", self.parameter_get)
        Protocol.register_cmd_callback("control_next", self.parameter_addressing_next)
        Protocol.register_cmd_callback("peakmeter", self.peakmeter_set)
        Protocol.register_cmd_callback("tuner", self.tuner_set)
        Protocol.register_cmd_callback("tuner_input", self.tuner_set_input)
        #Protocol.register_cmd_callback("pedalboard_save", self.save_current_pedalboard)
        #Protocol.register_cmd_callback("pedalboard_reset", self.reset_current_pedalboard)
        Protocol.register_cmd_callback("jack_cpu_load", self.jack_cpu_load)

#        self.host = factory(Host, FakeHost, DEV_HOST,
#                            "unix:///tmp/ingen.sock", self.host_callback)
        self.host = Host(os.environ.get("MOD_INGEN_SOCKET_URI", "unix:///tmp/ingen.sock"), self.host_callback)
        self.hmi = factory(HMI, FakeHMI, DEV_HMI,
                           HMI_SERIAL_PORT, HMI_BAUD_RATE, self.hmi_callback)

        self.recorder = Recorder()
        self.player = Player()
        self.bundlepath = None
        self.mute_state = True
        self.recording = None
        self.instances = []
        self.instance_mapper = InstanceIdMapper()
        self.engine_samplerate = 48000 # default value

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

        self._load_pb_hack = None
        self._save_waiter = None

    def reconnect(self):
        self.host.open_connection(self.host_callback)

    def websocket_opened(self, ws):
        self.websockets.append(ws)
        self.host.get("/graph")

    @gen.engine
    def host_callback(self):
        self.host_initialized = True

        def port_value_cb(instance, port, value):
            instance_id = self.instance_mapper.map(instance)
            if self._pedalboard.data['instances'].get(instance_id, False):
                self._pedalboard.parameter_set(instance_id, port, value)
                addrs = self._pedalboard.data['instances'][instance_id]['addressing']
                addr = addrs.get(port, None)
                if addr:
                    addr['value'] = value
                    act = addr['actuator']
                    self.parameter_addressing_load(*act)

        def position_cb(instance, x, y):
            pass

        def plugin_add_cb(instance, uri, x, y):
            if not instance in self.instances:
                self._pedalboard.add_instance(uri, self.instance_mapper.map(instance), x=x, y=y)
                self.instances.append(instance)

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

        def connection_add_cb(instance_a, port_a, instance_b, port_b):
            pass

        def connection_delete_cb(instance_a, port_a, instance_b, port_b):
            pass

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

        def sr_cb(value):
            self.engine_samplerate = value

        self.host.msg_callback = msg_cb
        self.host.samplerate_value_callback = sr_cb

        yield gen.Task(lambda callback: self.host.get_engine_info(callback=callback))

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

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

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

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

        yield gen.Task(lambda callback: self.host.initial_setup(callback=callback))

        self.host.position_callback = position_cb
        self.host.port_value_callback = port_value_cb
        self.host.plugin_add_callback = plugin_add_cb
        self.host.delete_callback = delete_cb
        self.host.connection_add_callback = connection_add_cb
        self.host.connection_delete_callback = connection_delete_cb

    def hmi_callback(self):
        if self.host_initialized:
            self.restore_last_pedalboard()
        logging.info("hmi initialized")
        self.hmi_initialized = True

    def reset(self, callback):
        gen = iter(copy.deepcopy(self.instances))
        def remove_all_plugins(r=True):
            try:
                self.remove(next(gen), remove_all_plugins)
            except StopIteration:
                callback(r)
        remove_all_plugins()

    def save_pedalboard(self, bundlepath, title, callback):
        def callback2(ok):
            self.bundlepath = bundlepath if ok else None
            callback(ok)

        self.host.set_pedalboard_name(title)
        self.host.save(os.path.join(bundlepath, "%s.ttl" % symbolify(title)), callback2)

    def load_pedalboard(self, bundlepath):
        # TODO
        self.bundlepath = bundlepath
        if self._load_pb_hack is not None:
            self._load_pb_hack(bundlepath)

    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)


    def tuner_set_input(self, input, callback):
        # TODO: implement
        self.disconnect("system:capture_%s" % self._tuner_port, "effect_%d:%s" % (TUNER, TUNER_PORT), lambda r:r, True)
        self._tuner_port = input
        self.connect("system:capture_%s" % input, "effect_%d:%s" % (TUNER, TUNER_PORT), callback, True)

    def tuner_set(self, status, callback):
        if "on" in status:
            self.tuner_on(callback)
        elif "off" in status:
            self.tuner_off(callback)

    def tuner_on(self, cb):
        def mon_tuner(ok):
            if ok:
                self.parameter_monitor(TUNER, TUNER_MON_PORT, ">=", 0, cb)

        def setup_tuner(ok):
            if ok:
                self._tuner = True
                self.connect("system:capture_%s" % self._tuner_port, "effect_%d:%s" % (TUNER, TUNER_PORT), mon_tuner, True)

        def mute_callback():
            self.add(TUNER_URI, TUNER, setup_tuner, True)
        self.mute(mute_callback)

    def tuner_off(self, cb):
        def callback():
            self.remove(TUNER, cb, True)
            self._tuner = False
        self.unmute(callback)

    def peakmeter_set(self, status, callback):
        if "on" in status:
            self.peakmeter_on(callback)
        elif "off" in status:
            self.peakmeter_off(callback)

    def peakmeter_on(self, cb):

        def mon_peak_in_l(ok):
            if ok:
                self.parameter_monitor(PEAKMETER_IN, PEAKMETER_MON_VALUE_L, ">=", -30, cb)
                self.parameter_monitor(PEAKMETER_IN, PEAKMETER_MON_PEAK_L, ">=", -30, cb)

        def mon_peak_in_r(ok):
            if ok:
                self.parameter_monitor(PEAKMETER_IN, PEAKMETER_MON_VALUE_R, ">=", -30, lambda r:None)
                self.parameter_monitor(PEAKMETER_IN, PEAKMETER_MON_PEAK_R, ">=", -30, lambda r:None)

        def mon_peak_out_l(ok):
            if ok:
                self.parameter_monitor(PEAKMETER_OUT, PEAKMETER_MON_VALUE_L, ">=", -30, lambda r:None)
                self.parameter_monitor(PEAKMETER_OUT, PEAKMETER_MON_PEAK_L, ">=", -30, lambda r:None)

        def mon_peak_out_r(ok):
            if ok:
                self.parameter_monitor(PEAKMETER_OUT, PEAKMETER_MON_VALUE_R, ">=", -30, lambda r:None)
                self.parameter_monitor(PEAKMETER_OUT, PEAKMETER_MON_PEAK_R, ">=", -30, lambda r:None)

        def setup_peak_in(ok):
            if ok:
                self.connect("system:capture_1", "effect_%d:%s" % (PEAKMETER_IN, PEAKMETER_L), mon_peak_in_l, True)
                self.connect("system:capture_2", "effect_%d:%s" % (PEAKMETER_IN, PEAKMETER_R), mon_peak_in_r, True)

        def setup_peak_out(ok):
            if ok:
                self._peakmeter = True
                for port in self._playback_1_connected_ports:
                    self.connect(port, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_L), mon_peak_out_l, True)
                for port in self._playback_2_connected_ports:
                    self.connect(port, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_R), mon_peak_out_r, True)

        self.add(PEAKMETER_URI, PEAKMETER_IN, setup_peak_in, True)
        self.add(PEAKMETER_URI, PEAKMETER_OUT, setup_peak_out, True)

    def peakmeter_off(self, cb):
        self.remove(PEAKMETER_IN, cb, True)
        self.remove(PEAKMETER_OUT, lambda r: None, True)
        self._tuner = False

    def hardware_connected(self, hwtyp, hwid, callback):
        callback(True)
        #open(os.path.join(HARDWARE_DIR, "%d_%d" % (hwtyp, hwid)), 'w')
        #callback(True)

    def hardware_disconnected(self, hwtype, hwid, callback):
        callback(True)
        #if os.path.exist():
        #    os.remove(os.path.join(HARDWARE_DIR, "%d_%d" % (hwtyp, hwid)), callback)
        #callback(True)

    # host commands

    def add(self, objid, instance, x, y, callback, loaded=False):
        self.host.add(objid, instance, x, y, callback)

    def remove(self, instance, callback, loaded=False):
        """
        affected_actuators = []
        if not loaded:
            affected_actuators = self._pedalboard.remove_instance(instance_id)
        def bufsize_callback(bufsize_changed):
            callback(True)
        def change_bufsize(ok):
            self.change_bufsize(self._pedalboard.get_bufsize(), bufsize_callback)

        def _callback(ok):
            if ok:
                self.hmi.control_rm(instance_id, ":all", change_bufsize)
                for addr in affected_actuators:
                    self.parameter_addressing_load(*addr)
            else:
                change_bufsize(ok)
        """
        if instance == "-1":
            self.reset(callback)
        else:
            self.host.remove(instance, callback)

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

    def connect(self, port_from, port_to, callback, loaded=False):
        #if not loaded:
        #    self._pedalboard.connect(port_from, port_to)

        # Cases below happen because we just save instance ID in pedalboard connection structure, not whole string
        #if not 'system' in port_from and not 'effect' in port_from:
        #    port_from = "effect_%s" % port_from
        #if not 'system' in port_to and not 'effect' in port_to:
        #    port_to = "effect_%s" % port_to

        #if "system" in port_to:
        #    def cb(result):
        #        if result:
        #            if port_to == "system:playback_1":
        #                self.connect(port_from, "effect_%d:%s" % (CLIPMETER_OUT, CLIPMETER_L), lambda r: r, True)
        #                self._playback_1_connected_ports.append(port_from)
        #                if self._peakmeter:
        #                    self.connect(port_from, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_L), lambda r: r, True)
        #            elif port_to == "system:playback_2":
        #                self.connect(port_from, "effect_%d:%s" % (CLIPMETER_OUT, CLIPMETER_R), lambda r: r, True)
        #                self._playback_2_connected_ports.append(port_from)
        #                if self._peakmeter:
        #                    self.connect(port_from, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_R), lambda r: r, True)
        #        callback(result)
        #else:
        #    cb = callback

        self.host.connect(port_from, port_to, callback)

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

    def disconnect(self, port_from, port_to, callback, loaded=False):
        """if not loaded:
            self._pedalboard.disconnect(port_from, port_to)

        port_from = self.format_port(port_from)
        port_to = self.format_port(port_to)

        if "system" in port_to:

            def cb(result):
                if result:
                    if port_to == "system:playback_1":
                        self.disconnect(port_from, "effect_%d:%s" % (CLIPMETER_OUT, CLIPMETER_L), lambda r: r, True)
                        if self._peakmeter:
                            self.disconnect(port_from, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_L), lambda r: r, True)
                        try:
                            self._playback_1_connected_ports.remove(port_from)
                        except ValueError:
                            pass
                    elif port_to == "system:playback_2":
                        self.disconnect(port_from, "effect_%d:%s" % (CLIPMETER_OUT, CLIPMETER_R), lambda r: r, True)
                        if self._peakmeter:
                            self.disconnect(port_from, "effect_%d:%s" % (PEAKMETER_OUT, PEAKMETER_R), lambda r: r, True)
                        try:
                            self._playback_2_connected_ports.remove(port_from)
                        except ValueError:
                            pass
                callback(result)
        else:
            cb = callback
        """
        self.host.disconnect(port_from, port_to, callback)

    def hmi_parameter_set(self, instance_id, port_id, value, callback):
        #self.browser.send(instance_id, port_id, value)
        self.parameter_set(instance_id, port_id, value, callback)

    def preset_load(self, instance_id, url, callback):
        self.host.preset_load(instance_id, url, callback)

    def parameter_set(self, port, value, callback, loaded=False):
        if port == ":bypass":
            # self.bypass(instance_id, value, callback)
            return
        #self.recorder.parameter(instance, port_id, value)
        self.host.param_set(port, value, callback)

    def parameter_get(self, port, callback):
        self.host.param_get(port, callback)

    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)

    # TODO: jack cpu load with ingen
    def jack_cpu_load(self, callback=lambda result: None):
        def cb(result):
            if result['ok']:
                pass
                #self.browser.send(99999, 'cpu_load', round(result['value']))
        self.host.cpu_load(cb)
    # END host commands

    # hmi commands
    def start_session(self, callback=None):
        self._playback_1_connected_ports = []
        self._playback_2_connected_ports = []

        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 bypass_address(self, instance_id, hardware_type, hardware_id, actuator_type, actuator_id, value, label,
                       callback, loaded=False):
        self.parameter_address(instance_id, ":bypass", 'switch', label, 6, "none", value,
                               1, 0, 0, hardware_type, hardware_id, actuator_type,
                               actuator_id, [], callback, loaded)

    def parameter_addressing_next(self, hardware_type, hardware_id, actuator_type, actuator_id, callback):
        addrs = self._pedalboard.addressings[(hardware_type, hardware_id, actuator_type, actuator_id)]
        if len(addrs['addrs']) > 0:
            addrs['idx'] = (addrs['idx'] + 1) % len(addrs['addrs'])
            callback(True)
            self.parameter_addressing_load(hardware_type, hardware_id, actuator_type, actuator_id,
                                           addrs['idx'])
            return True
        #elif len(addrs['addrs']) <= 0:
        #   self.hmi.control_clean(hardware_type, hardware_id, actuator_type, actuator_id)
        callback(True)
        return False

    def parameter_addressing_load(self, hw_type, hw_id, act_type, act_id, idx=None):
        addrs = self._pedalboard.addressings[(hw_type, hw_id, act_type, act_id)]
        if idx == None:
            idx = addrs['idx']
        try:
            addressing = addrs['addrs'][idx]
        except IndexError:
            return
        self.hmi.control_add(addressing['instance_id'], addressing['port_id'], addressing['label'],
                             addressing['type'], addressing['unit'], addressing['value'],
                             addressing['maximum'], addressing['minimum'], addressing['steps'],
                             addressing['actuator'][0], addressing['actuator'][1],
                             addressing['actuator'][2], addressing['actuator'][3], len(addrs['addrs']), idx+1,
                             addressing.get('options', []))


    def parameter_address(self, port, addressing_type, label, ctype,
                          unit, current_value, maximum, minimum, steps,
                          hardware_type, hardware_id, actuator_type, actuator_id,
                          options, callback, loaded=False):
        # TODO the IHM parameters set by hardware.js should be here!
        # The problem is that we need port data, and getting it now is expensive
        """
        instance_id: effect instance
        port_id: control port
        addressing_type: 'range', 'switch' or 'tap_tempo'
        label: lcd display label
        ctype: 0 linear, 1 logarithm, 2 enumeration, 3 toggled, 4 trigger, 5 tap tempo, 6 bypass
        unit: string representing the parameter unit (hz, bpm, seconds, etc)
        hardware_type: the hardware model
        hardware_id: the id of the hardware where we find this actuator
        actuator_type: the encoder button type
        actuator_id: the encoder button number
        options: array of options, each one being a tuple (value, label)
        """
        instance_id = self.instance_mapper.map(port.split("/")[0])
        port_id = port.split("/")[1]
        if (hardware_type == -1 and
            hardware_id == -1 and
            actuator_type == -1 and
            actuator_id == -1):
            if not loaded:
                a = self._pedalboard.parameter_unaddress(instance_id, port_id)
                if a:
                    if not self.parameter_addressing_next(a[0], a[1], a[2], a[3], callback):
                        self.hmi.control_rm(instance_id, port_id, lambda r:None)
                else:
                    callback(True)
            else:
                self.hmi.control_rm(instance_id, port_id, callback)
            return

        if not loaded:
            old = self._pedalboard.parameter_address(instance_id, port_id,
                                                     addressing_type,
                                                     label,
                                                     ctype,
                                                     unit,
                                                     current_value,
                                                     maximum,
                                                     minimum,
                                                     steps,
                                                     hardware_type,
                                                     hardware_id,
                                                     actuator_type,
                                                     actuator_id,
                                                     options)

            self.hmi.control_add(instance_id,
                                 port_id,
                                 label,
                                 ctype,
                                 unit,
                                 current_value,
                                 maximum,
                                 minimum,
                                 steps,
                                 hardware_type,
                                 hardware_id,
                                 actuator_type,
                                 actuator_id,
                                 len(self._pedalboard.addressings[(hardware_type, hardware_id, actuator_type, actuator_id)]['addrs']),
                                 len(self._pedalboard.addressings[(hardware_type, hardware_id, actuator_type, actuator_id)]['addrs']),
                                 options,
                                 callback)
            if old:
                self.parameter_addressing_load(*old)
        else:
            callback(True)


    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 ping(self, callback):
        self.hmi.ping(callback)

    def effect_position(self, instance, x, y):
        self.host.set_position(instance, x, y)
        #self._pedalboard.set_position(instance, x, y)

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

    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._pedalboard)

    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():
            ioloop.IOLoop.instance().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):
        self.set_audio_state(False, callback)
    def unmute(self, callback):
        self.set_audio_state(True, callback)

    def set_audio_state(self, state, callback):
        if self.mute_state == state:
            return callback()
        self.mute_state = state
        connections = self._pedalboard.data['connections']
        queue = []
        for connection in connections:
            if connection[2] == 'system' and connection[3].startswith('playback'):
                port_from = self.format_port(':'.join([str(x) for x in connection[:2]]))
                port_to = self.format_port(':'.join([str(x) for x in connection[2:]]))
                queue.append([port_from, port_to])
        def consume(result=None):
            if len(queue) == 0:
                return callback()
            nxt = queue.pop(0)
            if state:
                self.host.connect(nxt[0], nxt[1], consume)
            else:
                self.host.disconnect(nxt[0], nxt[1], consume)
        consume()

    def serialize_pedalboard(self):
        return self._pedalboard.serialize()

    def xrun(self, callback=None):
        cb = callback
        if not cb:
            cb = lambda r: r
        self.hmi.xrun(cb)
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