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)
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
class Addressing(object): def __init__(self, hmi): self.host = None self.hmi = hmi self.mapper = InstanceIdMapper() self.banks = [] self.instances = {} self._init_addressings() # Register HMI protocol callbacks Protocol.register_cmd_callback("hw_con", self.hmi_hardware_connected) Protocol.register_cmd_callback("hw_dis", self.hmi_hardware_disconnected) Protocol.register_cmd_callback("banks", self.hmi_list_banks) Protocol.register_cmd_callback("pedalboards", self.hmi_list_bank_pedalboards) Protocol.register_cmd_callback("pedalboard", self.hmi_load_bank_pedalboard) Protocol.register_cmd_callback("control_get", self.hmi_parameter_get) Protocol.register_cmd_callback("control_set", self.hmi_parameter_set) Protocol.register_cmd_callback("control_next", self.hmi_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) def clear(self): self.banks = [] self.instances = {} self._init_addressings() def get_addressings(self): addressings = {} for uri, addressing in self.addressings.items(): addrs = [] for addr in addressing['addrs']: addrs.append({ 'instance': self.mapper.get_instance(addr['instance_id']), 'port' : addr['port'], 'label' : addr['label'], 'minimum' : addr['minimum'], 'maximum' : addr['maximum'], 'steps' : addr['steps'], 'value' : addr['value'], }) addressings[uri] = addrs return addressings # ----------------------------------------------------------------------------------------------------------------- # Init our ingen host class # This is only called when the HMI responds to our initial ping (and it's thus initialized) # The reason for this being a separate init function is because we don't need it when HMI is off def init_host(self): # We need our own host instance so that messages get propagated correctly by ingen # Later on this code will be a separate application so it all fits anyway self.host = Host(os.getenv("MOD_INGEN_SOCKET_URI", "unix:///tmp/ingen.sock")) def loaded_callback(bundlepath): self._load_addressings(bundlepath) def plugin_added_callback(instance, uri, enabled, x, y): self._add_instance(instance, uri, not enabled) def plugin_removed_callback(instance): self._remove_instance(instance) def plugin_enabled_callback(instance, enabled): self._set_bypassed(instance, not enabled) def port_value_callback(port, value): instance, port = port.rsplit("/", 1) self._set_value(instance, port, value) self.host.loaded_callback = loaded_callback self.host.plugin_added_callback = plugin_added_callback self.host.plugin_removed_callback = plugin_removed_callback self.host.port_value_callback = port_value_callback self.host.open_connection_if_needed(self.host_callback) def host_callback(self): self.host.get("/graph") # ----------------------------------------------------------------------------------------------------------------- def _load_addressings(self, bundlepath): datafile = os.path.join(bundlepath, "addressings.json") if not os.path.exists(datafile): return with open(datafile, 'r') as fh: data = fh.read() data = json.loads(data) self._init_addressings() stopNow = False def callback(ok): if not ok: stopNow = True for actuator_uri in data: for addr in data[actuator_uri]: self.address(addr["instance"], addr["port"], actuator_uri, addr["label"], addr["maximum"], addr["minimum"], addr['value'], addr["steps"], callback) if stopNow: break if stopNow: break def _add_instance(self, instance, uri, bypassed): instance_id = self.mapper.get_id(instance) self.instances[instance] = { 'id': instance_id, 'instance': instance, 'uri': uri, 'bypassed': bypassed, 'addressing': {}, # symbol: addressing 'ports': {}, # symbol: value } logging.info('[addressing] Added instance %s' % instance) def _remove_instance(self, instance): instance_id = self.mapper.get_id(instance) # Remove the instance try: self.instances.pop(instance) except KeyError: logging.error('[addressing] Cannot remove unknown instance %s' % instance) # Remove addressings of that instance #affected_actuators = {} for actuator_uri, addressing in self.addressings.items(): i = 0 while i < len(addressing['addrs']): if addressing['addrs'][i].get('instance_id') == instance_id: addressing['addrs'].pop(i) if addressing['idx'] >= i: addressing['idx'] -= 1 #affected_actuators[actuator_uri] = addressing['idx'] else: i += 1 self.hmi.control_rm(instance_id, ":all") #for addr in affected_actuators: #self.parameter_addressing_load(*addr) logging.info('[addressing] Removed instance %s' % instance) #return [ list(act) + [idx] for act, idx in affected_actuators.items() ] def _set_bypassed(self, instance, bypassed): data = self.instances.get(instance, None) if data is None: return data['bypassed'] = bypassed addr = data['addressing'].get(':bypass', None) if addr is None: return addr['value'] = 1 if bypassed else 0 def _set_value(self, instance, port, value): data = self.instances.get(instance, None) if data is None: return data['ports'][port] = value addr = data['addressing'].get(port, None) if addr is None: return addr['value'] = value # ----------------------------------------------------------------------------------------------------------------- def address(self, instance, port, actuator_uri, label, maximum, minimum, value, steps, callback): instance_id = self.mapper.get_id(instance) old_actuator_uri = self._unaddress(instance, port) if (not actuator_uri) or actuator_uri == "null": self.hmi.control_rm(instance_id, port, callback) if old_actuator_uri is not None: old_actuator_hw = self._uri2hw_map[old_actuator_uri] self._address_next(old_actuator_hw) return data = self.instances.get(instance, None) if data is None: callback(False) return options = [] if port == ":bypass": ctype = ADDRESSING_CTYPE_BYPASS unit = "none" else: for port_info in get_plugin_info(data["uri"])["ports"]["control"]["input"]: if port_info["symbol"] != port: continue break else: callback(False) return pprops = port_info["properties"] unit = port_info["units"]["symbol"] if "symbol" in port_info["units"] else "none" if "toggled" in pprops: ctype = ADDRESSING_CTYPE_TOGGLED elif "integer" in pprops: ctype = ADDRESSING_CTYPE_INTEGER else: ctype = ADDRESSING_CTYPE_LINEAR if "logarithmic" in pprops: ctype |= ADDRESSING_CTYPE_LOGARITHMIC if "trigger" in pprops: ctype |= ADDRESSING_CTYPE_TRIGGER if "tap_tempo" in pprops: # TODO ctype |= ADDRESSING_CTYPE_TAP_TEMPO if len(port_info["scalePoints"]) >= 2: # and "enumeration" in pprops: ctype |= ADDRESSING_CTYPE_SCALE_POINTS|ADDRESSING_CTYPE_ENUMERATION if "enumeration" in pprops: ctype |= ADDRESSING_CTYPE_ENUMERATION for scalePoint in port_info["scalePoints"]: options.append((scalePoint["value"], scalePoint["label"])) del port_info, pprops addressing = { 'actuator_uri': actuator_uri, 'instance_id': instance_id, 'port': port, 'label': label, 'type': ctype, 'unit': unit, 'minimum': minimum, 'maximum': maximum, 'value': value, 'steps': steps, 'options': options, } self.instances[instance]['addressing'][port] = addressing self.addressings[actuator_uri]['addrs'].append(addressing) self.addressings[actuator_uri]['idx'] = len(self.addressings[actuator_uri]['addrs']) - 1 if old_actuator_uri is not None: self._addressing_load(old_actuator_uri) self._addressing_load(actuator_uri, callback) # ----------------------------------------------------------------------------------------------------------------- # HMI callbacks, called by HMI via serial def hmi_hardware_connected(self, hardware_type, hardware_id, callback): logging.info("hmi hardware connected") callback(True) def hmi_hardware_disconnected(self, hardware_type, hardware_id, callback): logging.info("hmi hardware disconnected") callback(True) def hmi_list_banks(self, callback): logging.info("hmi list banks") self.banks = list_banks() banks = " ".join('"%s" %d' % (bank['title'], i) for i,bank in enumerate(self.banks)) callback(True, banks) def hmi_list_bank_pedalboards(self, bank_id, callback): logging.info("hmi list bank pedalboards") if bank_id < len(self.banks): #pedalboards = " ".join('"%s" %d' % (pb['title'], i) for i,pb in enumerate(self.banks[bank_id]['pedalboards'])) pedalboards = " ".join('"%s" "%s"' % (pb['title'], pb['uri']) for pb in self.banks[bank_id]['pedalboards']) else: pedalboards = "" callback(True, pedalboards) def hmi_load_bank_pedalboard(self, bank_id, pedalboard_uri, callback): logging.info("hmi load bank pedalboard") #if bank_id >= len(self.banks): #print("ERROR in addressing.py: bank id out of bounds") #return #pedalboards = self.banks[bank_id]['pedalboards'] #if pedalboard_id >= len(pedalboards): #print("ERROR in addressing.py: pedalboard id out of bounds") #return #uri = pedalboards[pedalboard_id]['uri'] self.host.load_uri(pedalboard_uri) def hmi_parameter_get(self, instance_id, port, callback): logging.info("hmi parameter get") instance = self.mapper.get_instance(instance_id) callback(self.instances[instance]['ports'][port]) def hmi_parameter_set(self, instance_id, port, value, callback=None): logging.info("hmi parameter set") instance = self.mapper.get_instance(instance_id) if port == ":bypass": self._set_bypassed(instance, bool(value)) if self.host is not None: self.host.enable(instance, not value, callback) else: self._set_value(instance, port, value) if self.host is not None: self.host.param_set("%s/%s" % (instance, port), value, callback) def hmi_parameter_addressing_next(self, hardware_type, hardware_id, actuator_type, actuator_id, callback): logging.info("hmi parameter addressing next") actuator_hw = (hardware_type, hardware_id, actuator_type, actuator_id) self._address_next(actuator_hw, 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 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 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 _init_addressings(self): # 'self.addressings' uses a structure like this: # "/hmi/knob1": {'addrs': [], 'idx': 0} self.addressings = dict((act["uri"], {'idx': 0, 'addrs': []}) for act in get_hardware()["actuators"]) # Store all possible hardcoded values self._hw2uri_map = {} self._uri2hw_map = {} for i in range(0, 4): knob_hw = (HARDWARE_TYPE_MOD, 0, ACTUATOR_TYPE_KNOB, i) foot_hw = (HARDWARE_TYPE_MOD, 0, ACTUATOR_TYPE_FOOTSWITCH, i) knob_uri = "/hmi/knob%i" % (i+1) foot_uri = "/hmi/footswitch%i" % (i+1) self._hw2uri_map[knob_hw] = knob_uri self._hw2uri_map[foot_hw] = foot_uri self._uri2hw_map[knob_uri] = knob_hw self._uri2hw_map[foot_uri] = foot_hw # ----------------------------------------------------------------------------------------------------------------- def _addressing_load(self, actuator_uri, callback=None): addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] try: addressing = addressings_addrs[addressings_idx] except IndexError: return actuator_hw = self._uri2hw_map[actuator_uri] self.hmi.control_add(addressing['instance_id'], addressing['port'], addressing['label'], addressing['type'], addressing['unit'], addressing['value'], addressing['maximum'], addressing['minimum'], addressing['steps'], actuator_hw[0], actuator_hw[1], actuator_hw[2], actuator_hw[3], len(addressings_addrs), # num controllers addressings_idx+1, # index addressing['options'], callback) def _address_next(self, actuator_hw, callback=lambda r:r): actuator_uri = self._hw2uri_map[actuator_hw] addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] if len(addressings_addrs) > 0: addressings['idx'] = (addressings['idx'] + 1) % len(addressings_addrs) callback(True) self._addressing_load(actuator_uri) else: callback(True) self.hmi.control_clean(actuator_hw[0], actuator_hw[1], actuator_hw[2], actuator_hw[3]) def _unaddress(self, instance, port): data = self.instances.get(instance, None) if data is None: return None addressing = data['addressing'].pop(port, None) if addressing is None: return None actuator_uri = addressing['actuator_uri'] addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] index = addressings_addrs.index(addressing) addressings_addrs.pop(index) # FIXME ? if addressings_idx >= index: addressings['idx'] -= 1 #if index <= addressings_idx: #addressings['idx'] = addressings_idx - 1 return actuator_uri
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
class Addressing(object): def __init__(self, hmi): self.host = None self.hmi = hmi self.mapper = InstanceIdMapper() self.banks = [] self.instances = {} self._init_addressings() # Register HMI protocol callbacks Protocol.register_cmd_callback("hw_con", self.hmi_hardware_connected) Protocol.register_cmd_callback("hw_dis", self.hmi_hardware_disconnected) Protocol.register_cmd_callback("banks", self.hmi_list_banks) Protocol.register_cmd_callback("pedalboards", self.hmi_list_bank_pedalboards) Protocol.register_cmd_callback("pedalboard", self.hmi_load_bank_pedalboard) Protocol.register_cmd_callback("control_get", self.hmi_parameter_get) Protocol.register_cmd_callback("control_set", self.hmi_parameter_set) Protocol.register_cmd_callback("control_next", self.hmi_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) def clear(self): self.banks = [] self.instances = {} self._init_addressings() def get_addressings(self): addressings = {} for uri, addressing in self.addressings.items(): addrs = [] for addr in addressing['addrs']: addrs.append({ 'instance': self.mapper.get_instance(addr['instance_id']), 'port': addr['port'], 'label': addr['label'], 'minimum': addr['minimum'], 'maximum': addr['maximum'], 'steps': addr['steps'], 'value': addr['value'], }) addressings[uri] = addrs return addressings # ----------------------------------------------------------------------------------------------------------------- # Init our ingen host class # This is only called when the HMI responds to our initial ping (and it's thus initialized) # The reason for this being a separate init function is because we don't need it when HMI is off def init_host(self): # We need our own host instance so that messages get propagated correctly by ingen # Later on this code will be a separate application so it all fits anyway self.host = Host( os.getenv("MOD_INGEN_SOCKET_URI", "unix:///tmp/ingen.sock")) def loaded_callback(bundlepath): self._load_addressings(bundlepath) def plugin_added_callback(instance, uri, enabled, x, y): self._add_instance(instance, uri, not enabled) def plugin_removed_callback(instance): self._remove_instance(instance) def plugin_enabled_callback(instance, enabled): self._set_bypassed(instance, not enabled) def port_value_callback(port, value): instance, port = port.rsplit("/", 1) self._set_value(instance, port, value) self.host.loaded_callback = loaded_callback self.host.plugin_added_callback = plugin_added_callback self.host.plugin_removed_callback = plugin_removed_callback self.host.port_value_callback = port_value_callback self.host.open_connection_if_needed(self.host_callback) def host_callback(self): self.host.get("/graph") # ----------------------------------------------------------------------------------------------------------------- def _load_addressings(self, bundlepath): datafile = os.path.join(bundlepath, "addressings.json") if not os.path.exists(datafile): return with open(datafile, 'r') as fh: data = fh.read() data = json.loads(data) self._init_addressings() stopNow = False def callback(ok): if not ok: stopNow = True for actuator_uri in data: for addr in data[actuator_uri]: self.address(addr["instance"], addr["port"], actuator_uri, addr["label"], addr["maximum"], addr["minimum"], addr['value'], addr["steps"], callback) if stopNow: break if stopNow: break def _add_instance(self, instance, uri, bypassed): instance_id = self.mapper.get_id(instance) self.instances[instance] = { 'id': instance_id, 'instance': instance, 'uri': uri, 'bypassed': bypassed, 'addressing': {}, # symbol: addressing 'ports': {}, # symbol: value } logging.info('[addressing] Added instance %s' % instance) def _remove_instance(self, instance): instance_id = self.mapper.get_id(instance) # Remove the instance try: self.instances.pop(instance) except KeyError: logging.error('[addressing] Cannot remove unknown instance %s' % instance) # Remove addressings of that instance #affected_actuators = {} for actuator_uri, addressing in self.addressings.items(): i = 0 while i < len(addressing['addrs']): if addressing['addrs'][i].get('instance_id') == instance_id: addressing['addrs'].pop(i) if addressing['idx'] >= i: addressing['idx'] -= 1 #affected_actuators[actuator_uri] = addressing['idx'] else: i += 1 self.hmi.control_rm(instance_id, ":all") #for addr in affected_actuators: #self.parameter_addressing_load(*addr) logging.info('[addressing] Removed instance %s' % instance) #return [ list(act) + [idx] for act, idx in affected_actuators.items() ] def _set_bypassed(self, instance, bypassed): data = self.instances.get(instance, None) if data is None: return data['bypassed'] = bypassed addr = data['addressing'].get(':bypass', None) if addr is None: return addr['value'] = 1 if bypassed else 0 def _set_value(self, instance, port, value): data = self.instances.get(instance, None) if data is None: return data['ports'][port] = value addr = data['addressing'].get(port, None) if addr is None: return addr['value'] = value # ----------------------------------------------------------------------------------------------------------------- def address(self, instance, port, actuator_uri, label, maximum, minimum, value, steps, callback): instance_id = self.mapper.get_id(instance) old_actuator_uri = self._unaddress(instance, port) if (not actuator_uri) or actuator_uri == "null": self.hmi.control_rm(instance_id, port, callback) if old_actuator_uri is not None: old_actuator_hw = self._uri2hw_map[old_actuator_uri] self._address_next(old_actuator_hw) return data = self.instances.get(instance, None) if data is None: callback(False) return options = [] if port == ":bypass": ctype = ADDRESSING_CTYPE_BYPASS unit = "none" else: for port_info in get_plugin_info( data["uri"])["ports"]["control"]["input"]: if port_info["symbol"] != port: continue break else: callback(False) return pprops = port_info["properties"] unit = port_info["units"]["symbol"] if "symbol" in port_info[ "units"] else "none" if "toggled" in pprops: ctype = ADDRESSING_CTYPE_TOGGLED elif "integer" in pprops: ctype = ADDRESSING_CTYPE_INTEGER else: ctype = ADDRESSING_CTYPE_LINEAR if "logarithmic" in pprops: ctype |= ADDRESSING_CTYPE_LOGARITHMIC if "trigger" in pprops: ctype |= ADDRESSING_CTYPE_TRIGGER if "tap_tempo" in pprops: # TODO ctype |= ADDRESSING_CTYPE_TAP_TEMPO if len(port_info["scalePoints"] ) >= 2: # and "enumeration" in pprops: ctype |= ADDRESSING_CTYPE_SCALE_POINTS | ADDRESSING_CTYPE_ENUMERATION if "enumeration" in pprops: ctype |= ADDRESSING_CTYPE_ENUMERATION for scalePoint in port_info["scalePoints"]: options.append((scalePoint["value"], scalePoint["label"])) del port_info, pprops addressing = { 'actuator_uri': actuator_uri, 'instance_id': instance_id, 'port': port, 'label': label, 'type': ctype, 'unit': unit, 'minimum': minimum, 'maximum': maximum, 'value': value, 'steps': steps, 'options': options, } self.instances[instance]['addressing'][port] = addressing self.addressings[actuator_uri]['addrs'].append(addressing) self.addressings[actuator_uri]['idx'] = len( self.addressings[actuator_uri]['addrs']) - 1 if old_actuator_uri is not None: self._addressing_load(old_actuator_uri) self._addressing_load(actuator_uri, callback) # ----------------------------------------------------------------------------------------------------------------- # HMI callbacks, called by HMI via serial def hmi_hardware_connected(self, hardware_type, hardware_id, callback): logging.info("hmi hardware connected") callback(True) def hmi_hardware_disconnected(self, hardware_type, hardware_id, callback): logging.info("hmi hardware disconnected") callback(True) def hmi_list_banks(self, callback): logging.info("hmi list banks") self.banks = list_banks() banks = " ".join('"%s" %d' % (bank['title'], i) for i, bank in enumerate(self.banks)) callback(True, banks) def hmi_list_bank_pedalboards(self, bank_id, callback): logging.info("hmi list bank pedalboards") if bank_id < len(self.banks): #pedalboards = " ".join('"%s" %d' % (pb['title'], i) for i,pb in enumerate(self.banks[bank_id]['pedalboards'])) pedalboards = " ".join( '"%s" "%s"' % (pb['title'], pb['uri']) for pb in self.banks[bank_id]['pedalboards']) else: pedalboards = "" callback(True, pedalboards) def hmi_load_bank_pedalboard(self, bank_id, pedalboard_uri, callback): logging.info("hmi load bank pedalboard") #if bank_id >= len(self.banks): #print("ERROR in addressing.py: bank id out of bounds") #return #pedalboards = self.banks[bank_id]['pedalboards'] #if pedalboard_id >= len(pedalboards): #print("ERROR in addressing.py: pedalboard id out of bounds") #return #uri = pedalboards[pedalboard_id]['uri'] self.host.load_uri(pedalboard_uri) def hmi_parameter_get(self, instance_id, port, callback): logging.info("hmi parameter get") instance = self.mapper.get_instance(instance_id) callback(self.instances[instance]['ports'][port]) def hmi_parameter_set(self, instance_id, port, value, callback=None): logging.info("hmi parameter set") instance = self.mapper.get_instance(instance_id) if port == ":bypass": self._set_bypassed(instance, bool(value)) if self.host is not None: self.host.enable(instance, not value, callback) else: self._set_value(instance, port, value) if self.host is not None: self.host.param_set("%s/%s" % (instance, port), value, callback) def hmi_parameter_addressing_next(self, hardware_type, hardware_id, actuator_type, actuator_id, callback): logging.info("hmi parameter addressing next") actuator_hw = (hardware_type, hardware_id, actuator_type, actuator_id) self._address_next(actuator_hw, 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 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 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 _init_addressings(self): # 'self.addressings' uses a structure like this: # "/hmi/knob1": {'addrs': [], 'idx': 0} self.addressings = dict((act["uri"], { 'idx': 0, 'addrs': [] }) for act in get_hardware()["actuators"]) # Store all possible hardcoded values self._hw2uri_map = {} self._uri2hw_map = {} for i in range(0, 4): knob_hw = (HARDWARE_TYPE_MOD, 0, ACTUATOR_TYPE_KNOB, i) foot_hw = (HARDWARE_TYPE_MOD, 0, ACTUATOR_TYPE_FOOTSWITCH, i) knob_uri = "/hmi/knob%i" % (i + 1) foot_uri = "/hmi/footswitch%i" % (i + 1) self._hw2uri_map[knob_hw] = knob_uri self._hw2uri_map[foot_hw] = foot_uri self._uri2hw_map[knob_uri] = knob_hw self._uri2hw_map[foot_uri] = foot_hw # ----------------------------------------------------------------------------------------------------------------- def _addressing_load(self, actuator_uri, callback=None): addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] try: addressing = addressings_addrs[addressings_idx] except IndexError: return actuator_hw = self._uri2hw_map[actuator_uri] self.hmi.control_add( addressing['instance_id'], addressing['port'], addressing['label'], addressing['type'], addressing['unit'], addressing['value'], addressing['maximum'], addressing['minimum'], addressing['steps'], actuator_hw[0], actuator_hw[1], actuator_hw[2], actuator_hw[3], len(addressings_addrs), # num controllers addressings_idx + 1, # index addressing['options'], callback) def _address_next(self, actuator_hw, callback=lambda r: r): actuator_uri = self._hw2uri_map[actuator_hw] addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] if len(addressings_addrs) > 0: addressings['idx'] = (addressings['idx'] + 1) % len(addressings_addrs) callback(True) self._addressing_load(actuator_uri) else: callback(True) self.hmi.control_clean(actuator_hw[0], actuator_hw[1], actuator_hw[2], actuator_hw[3]) def _unaddress(self, instance, port): data = self.instances.get(instance, None) if data is None: return None addressing = data['addressing'].pop(port, None) if addressing is None: return None actuator_uri = addressing['actuator_uri'] addressings = self.addressings[actuator_uri] addressings_addrs = addressings['addrs'] addressings_idx = addressings['idx'] index = addressings_addrs.index(addressing) addressings_addrs.pop(index) # FIXME ? if addressings_idx >= index: addressings['idx'] -= 1 #if index <= addressings_idx: #addressings['idx'] = addressings_idx - 1 return actuator_uri