diff --git a/zyngine/zynthian_controller.py b/zyngine/zynthian_controller.py index 33e4e117f..26c70b708 100644 --- a/zyngine/zynthian_controller.py +++ b/zyngine/zynthian_controller.py @@ -118,6 +118,9 @@ def setup_controller(self, chan, cc, val, maxval=127): self.labels=maxval self.value_max=len(maxval)-1 + def set_midi_chan(self, chan): + self.midi_chan=chan + def get_ctrl_array(self): tit=self.short_name if self.midi_chan: diff --git a/zyngine/zynthian_engine_fluidsynth.py b/zyngine/zynthian_engine_fluidsynth.py index 07c852fde..7d0d8e6e5 100644 --- a/zyngine/zynthian_engine_fluidsynth.py +++ b/zyngine/zynthian_engine_fluidsynth.py @@ -52,7 +52,7 @@ def __init__(self, zyngui=None): super().__init__(zyngui) self.name="FluidSynth" self.nickname="FS" - self.command=("/usr/bin/fluidsynth", "-p", "fluidsynth", "-a", "jack", "-m", "jack" ,"-g", "1", "-j", "-o", "synth.midi-bank-select", "mma", "synth.cpu-cores", "3") + self.command=("/usr/bin/fluidsynth", "-p", "fluidsynth", "-a", "jack", "-m", "jack" ,"-g", "1", "-j", "-o", "synth.midi-bank-select=mma", "-o", "synth.cpu-cores=3", "-o", "synth.polyphony=64") self.soundfont_dirs=[ ('_', os.getcwd()+"/data/soundfonts/sf2"), @@ -79,12 +79,13 @@ def stop(self): def add_layer(self, layer): super().add_layer(layer) - layer.part_i=self.get_free_parts()[0] - logging.debug("ADD LAYER => PART %s" % layer.part_i) + layer.part_i=None + self.setup_router(layer) def del_layer(self, layer): super().del_layer(layer) - self.set_all_midi_routes() + if layer.part_i is not None: + self.set_all_midi_routes() self.unload_unused_soundfonts() # --------------------------------------------------------------------------- @@ -92,7 +93,7 @@ def del_layer(self, layer): # --------------------------------------------------------------------------- def set_midi_chan(self, layer): - self.set_all_midi_routes() + self.setup_router(layer) # --------------------------------------------------------------------------- # Bank Management @@ -113,7 +114,7 @@ def get_preset_list(self, bank): logging.info("Getting Preset List for %s" % bank[2]) preset_list=[] sfi=self.soundfont_index[bank[0]] - lines=self.proc_cmd("inst %d" % sfi) + lines=self.proc_cmd("inst %d" % sfi, 10) for f in lines: try: prg=int(f[4:7]) @@ -136,7 +137,6 @@ def set_preset(self, layer, preset): midi_prg=preset[1][2] logging.debug("Set Preset => Layer: %d, SoundFont: %d, Bank: %d, Program: %d" % (layer.part_i,sfi,midi_bank,midi_prg)) self.proc_cmd("select %d %d %d %d" % (layer.part_i,sfi,midi_bank,midi_prg)) - self.set_all_midi_routes() else: logging.warning("Can't set Instrument before loading SoundFont") @@ -158,6 +158,12 @@ def load_soundfont(self, sf): self.soundfont_count=self.soundfont_count+1 logging.info("Load SoundFont => %s (%d)" % (sf,self.soundfont_count)) self.proc_cmd("load \"%s\"" % sf, 20) + + # Reselect presets for all layers to prevent instrument change + for layer in self.layers: + if layer.preset_info: + self.set_preset(layer, layer.preset_info) + self.soundfont_index[sf]=self.soundfont_count return self.soundfont_count @@ -169,14 +175,28 @@ def set_midi_channel(self, layer): if layer.part_i is not None: liblo.send(self.osc_target, "/part%d/Prcvchn" % layer.part_i, layer.get_midi_chan()) + def setup_router(self, layer): + if layer.part_i is not None: + # Clear and recreate all routes if the routes for this layer were set already + self.set_all_midi_routes() + else: + # No need to clear routes if there is the only layer to add + try: + layer.part_i=self.get_free_parts()[0] + logging.debug("ADD LAYER => PART %s" % layer.part_i) + except: + logging.error("ADD LAYER => NO FREE PARTS!") + self.set_layer_midi_routes(layer) + def unload_unused_soundfonts(self): #Make a copy of soundfont index and remove used soundfonts sf_unload=copy.copy(self.soundfont_index) for layer in self.layers: bi=layer.bank_info - if bi[2] and bi[0] in sf_unload: - #print("Skip "+bi[0]+"("+str(sf_unload[bi[0]])+")") - del sf_unload[bi[0]] + if bi is not None: + if bi[2] and bi[0] in sf_unload: + #print("Skip "+bi[0]+"("+str(sf_unload[bi[0]])+")") + del sf_unload[bi[0]] #Then, remove the remaining ;-) for sf,sfi in sf_unload.items(): logging.info("Unload SoundFont => %d" % sfi) @@ -186,8 +206,20 @@ def unload_unused_soundfonts(self): def set_layer_midi_routes(self, layer): if layer.part_i is not None: midich=layer.get_midi_chan() - self.proc_cmd("router_begin note\nrouter_chan %d %d 0 %d\nrouter_end" % (midich,midich,layer.part_i)) - self.proc_cmd("router_begin cc\nrouter_chan %d %d 0 %d\nrouter_end" % (midich,midich,layer.part_i)) + self.proc_cmd( + "router_begin note\n" + "router_chan {0} {0} 0 {1}\n" + "router_end\n" + "router_begin cc\n" + "router_chan {0} {0} 0 {1}\n" + "router_end\n" + "router_begin pbend\n" + "router_chan {0} {0} 0 {1}\n" + "router_end\n" + "router_begin prog\n" + "router_chan {0} {0} 0 {1}\n" + "router_end".format(midich, layer.part_i) + ) def set_all_midi_routes(self): self.clear_midi_routes() diff --git a/zyngine/zynthian_engine_linuxsampler.py b/zyngine/zynthian_engine_linuxsampler.py index 4dbc8201c..61e6d1d5c 100644 --- a/zyngine/zynthian_engine_linuxsampler.py +++ b/zyngine/zynthian_engine_linuxsampler.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- #****************************************************************************** # ZYNTHIAN PROJECT: Zynthian Engine (zynthian_engine_linuxsampler) # @@ -60,16 +60,17 @@ class zynthian_engine_linuxsampler(zynthian_engine): # --------------------------------------------------------------------------- _ladspa_plugins=[ + ('tap_chorusflanger', { 'lib': '/usr/lib/ladspa/tap_chorusflanger.so', 'id': None }), #('mod_delay', { 'lib': '/usr/lib/ladspa/mod_delay_1419.so', 'id': None }) => BAD #('revdelay', { 'lib': '/usr/lib/ladspa/revdelay_1605.so', 'id': None }), => BAD #('vocoder', { 'lib': '/usr/lib/ladspa/vocoder.so', 'id': None }), #('g2reverb', { 'lib': '/usr/lib/ladspa/g2reverb.so', 'id': None }), - ('tap_reverb', { 'lib': '/usr/lib/ladspa/tap_reverb.so', 'id': None }), #('tap_vibrato', { 'lib': '/usr/lib/ladspa/tap_vibrato.so', 'id': None }), => BAD #('tap_tremolo', { 'lib': '/usr/lib/ladspa/tap_tremolo.so', 'id': None }), => BAD #('caps', { 'lib': '/usr/lib/ladspa/caps.so', 'id': None }), => BAD #('rubberband', { 'lib': '/usr/lib/ladspa/ladspa-rubberband.so', 'id': None }), => BAD - ('tap_chorusflanger', { 'lib': '/usr/lib/ladspa/tap_chorusflanger.so', 'id': None }) + ('tap_reverb', { 'lib': '/usr/lib/ladspa/tap_reverb.so', 'id': None }), + #('tap_echo', { 'lib': '/usr/lib/ladspa/tap_echo.so', 'id': None }) ] # --------------------------------------------------------------------------- @@ -83,8 +84,8 @@ def __init__(self, zyngui=None): self.sock=None self.port=6688 - self.command=("/usr/bin/linuxsampler", "--lscp-port", str(self.port)) - os.environ["LADSPA_PATH"]="/usr/lib64/ladspa" + self.command=("linuxsampler", "--lscp-port", str(self.port)) + #os.environ["LADSPA_PATH"]="/usr/lib/ladspa" self.ls_chans={} self.ls_effects=OrderedDict(self._ladspa_plugins) @@ -96,9 +97,10 @@ def __init__(self, zyngui=None): ('MySFZ', os.getcwd()+"/my-data/soundfonts/sfz"), ('MyGIG', os.getcwd()+"/my-data/soundfonts/gig") ] - + self.lscp_v1_6_supported=False self.start() self.lscp_connect() + self.lscp_get_version() self.reset() def reset(self): @@ -124,10 +126,23 @@ def lscp_connect(self): sleep(0.25) i+=1 return self.sock - + + def lscp_get_version(self): + sv_info=self.lscp_send_multi("GET SERVER INFO") + if 'PROTOCOL_VERSION' in sv_info: + match=re.match(r"(?P\d+)\.(?P\d+).*",sv_info['PROTOCOL_VERSION']) + if match: + version_major=int(match['major']) + version_minor=int(match['minor']) + if version_major>1 or (version_major==1 and version_minor>=6): + self.lscp_v1_6_supported=True + def lscp_send(self,data): command=command+"\r\n" - self.sock.send(data.encode()) + try: + self.sock.send(data.encode()) + except Exception as err: + logging.error("FAILED lscp_send: %s" % err) def lscp_get_result_index(self, result): parts=result.split('[') @@ -137,12 +152,17 @@ def lscp_get_result_index(self, result): def lscp_send_single(self, command): self.start_loading() - logging.debug("LSCP SEND => %s" % command) + #logging.debug("LSCP SEND => %s" % command) command=command+"\r\n" - self.sock.send(command.encode()) - line=self.sock.recv(4096) + try: + self.sock.send(command.encode()) + line=self.sock.recv(4096) + except Exception as err: + logging.error("FAILED lscp_send_single(%s): %s" % (command,err)) + self.stop_loading() + return None line=line.decode() - logging.debug("LSCP RECEIVE => %s" % line) + #logging.debug("LSCP RECEIVE => %s" % line) if line[0:2]=="OK": result=self.lscp_get_result_index(line) elif line[0:3]=="ERR": @@ -158,14 +178,19 @@ def lscp_send_single(self, command): def lscp_send_multi(self, command): self.start_loading() - logging.debug("LSCP SEND => %s" % command) + #logging.debug("LSCP SEND => %s" % command) command=command+"\r\n" - self.sock.send(command.encode()) - result=self.sock.recv(4096) + try: + self.sock.send(command.encode()) + result=self.sock.recv(4096) + except Exception as err: + logging.error("FAILED lscp_send_multi(%s): %s" % (command,err)) + self.stop_loading() + return None lines=result.decode().split("\r\n") result=OrderedDict() for line in lines: - logging.debug("LSCP RECEIVE => %s" % line) + #logging.debug("LSCP RECEIVE => %s" % line) if line[0:2]=="OK": result=self.lscp_get_result_index(line) elif line[0:3]=="ERR": @@ -189,6 +214,9 @@ def lscp_send_multi(self, command): def add_layer(self, layer): super().add_layer(layer) layer.ls_chan_info=None + self.ls_set_channel(layer) + self.set_midi_chan(layer) + layer.refresh_flag=True def del_layer(self, layer): super().del_layer(layer) @@ -199,8 +227,8 @@ def del_layer(self, layer): # --------------------------------------------------------------------------- def set_midi_chan(self, layer): - ls_chan_id=layer.ls_chan_info['chan_id'] - if ls_chan_id is not None: + if layer.ls_chan_info: + ls_chan_id=layer.ls_chan_info['chan_id'] self.lscp_send_single("SET CHANNEL MIDI_INPUT_CHANNEL %d %d" % (ls_chan_id,layer.get_midi_chan())) # --------------------------------------------------------------------------- @@ -217,6 +245,8 @@ def set_bank(self, layer, bank): # Preset Management # --------------------------------------------------------------------------- + _exclude_sfz = re.compile(r"[MOPRSTV][1-9]?l?\.sfz") + def get_preset_list(self, bank): self.start_loading() logging.info("Getting Preset List for %s" % bank[2]) @@ -231,16 +261,18 @@ def get_preset_list(self, bank): lines=output.split('\n') for f in lines: if f: - filename, filext = os.path.splitext(f) - title=filename[len(preset_dpath)+1:].replace('_', ' ') - engine=filext[1:].lower() - preset_list.append((i,[0,0,0],title,f,engine)) - i=i+1 + filehead,filetail=os.path.split(f) + if not self._exclude_sfz.fullmatch(filetail): + filename,filext=os.path.splitext(f) + title=filename[len(preset_dpath)+1:].replace('_', ' ') + engine=filext[1:].lower() + preset_list.append((i,[0,0,0],title,f,engine)) + i=i+1 self.stop_loading() return preset_list def set_preset(self, layer, preset): - self.ls_set_channel(layer, preset[4], preset[3]) + self.ls_set_preset(layer, preset[4], preset[3]) # --------------------------------------------------------------------------- # Controllers Management @@ -308,11 +340,11 @@ def get_controllers_dict(self, layer): 'graph_path': str(fx_info['id'])+'/'+str(i) } zctrls[ctrl_symbol]=zynthian_controller(self,ctrl_symbol,ctrl_name,ctrl_options) - scrctrls.append(ctrl_symbol) if len(scrctrls)==4: self._ctrl_screens.append([fx_name+':'+str(j),scrctrls]) scrctrls=[] j=j+1 + scrctrls.append(ctrl_symbol) self._ctrl_screens.append([fx_name+':'+str(j),scrctrls]) return zctrls @@ -321,6 +353,7 @@ def send_controller_value(self, zctrl): parts=zctrl.graph_path.split('/') fx_id=parts[0] fx_ctrl_i=parts[1] + logging.debug("LSCP: Sending controller %s => %s" % (zctrl.name,zctrl.value)) self.lscp_send_single("SET EFFECT_INSTANCE_INPUT_CONTROL VALUE %s %s %s" % (fx_id,fx_ctrl_i,zctrl.value)) # --------------------------------------------------------------------------- @@ -348,20 +381,24 @@ def ls_init(self): # Global volume level self.lscp_send_single("SET VOLUME 0.45") - def ls_set_channel(self, layer, ls_engine, fpath): - self.ls_unset_channel(layer) - midi_chan=layer.get_midi_chan() + def ls_set_channel(self, layer): + # Adding new channel ls_chan_id=self.lscp_send_single("ADD CHANNEL") if ls_chan_id>=0: self.lscp_send_single("SET CHANNEL AUDIO_OUTPUT_DEVICE %d %d" % (ls_chan_id,self.ls_audio_device_id)) - self.lscp_send_single("SET CHANNEL MIDI_INPUT_DEVICE %d %d" % (ls_chan_id,self.ls_midi_device_id)) - self.lscp_send_single("SET CHANNEL MIDI_INPUT_PORT %d %d" % (ls_chan_id,0)) - self.lscp_send_single("SET CHANNEL MIDI_INPUT_CHANNEL %d %d" % (ls_chan_id,midi_chan)) - self.lscp_send_single("LOAD ENGINE %s %d" % (ls_engine, ls_chan_id)) - self.lscp_send_single("LOAD INSTRUMENT NON_MODAL '%s' 0 %d" % (fpath,ls_chan_id)) - self.lscp_send_single("SET CHANNEL AUDIO_OUTPUT_CHANNEL %d 0 0" % ls_chan_id) - self.lscp_send_single("SET CHANNEL AUDIO_OUTPUT_CHANNEL %d 1 1" % ls_chan_id) - self.lscp_send_single("SET CHANNEL VOLUME %d 1" % ls_chan_id) + # Use "ADD CHANNEL MIDI_INPUT" + if self.lscp_v1_6_supported: + self.lscp_send_single("ADD CHANNEL MIDI_INPUT %d %d 0" % (ls_chan_id,self.ls_midi_device_id)) + else: + self.lscp_send_single("SET CHANNEL MIDI_INPUT_DEVICE %d %d" % (ls_chan_id,self.ls_midi_device_id)) + self.lscp_send_single("SET CHANNEL MIDI_INPUT_PORT %d %d" % (ls_chan_id,0)) + + #self.lscp_send_single("SET CHANNEL MIDI_INPUT_CHANNEL %d %d" % (ls_chan_id,layer.get_midi_chan())) + + #TODO: need? + #self.lscp_send_single("SET CHANNEL AUDIO_OUTPUT_CHANNEL %d 0 0" % ls_chan_id) + #self.lscp_send_single("SET CHANNEL AUDIO_OUTPUT_CHANNEL %d 1 1" % ls_chan_id) + #self.lscp_send_single("SET CHANNEL VOLUME %d 1" % ls_chan_id) #self.lscp_send_single("SET CHANNEL MIDI_INSTRUMENT_MAP %d 0" % ls_chan_id) #Setup Effect Chain @@ -388,37 +425,79 @@ def ls_set_channel(self, layer, ls_engine, fpath): except zyngine_lscp_warning as warn: logging.warning(warn) - fx_send_id=self.lscp_send_single("CREATE FX_SEND %d %d" % (ls_chan_id, 12)) - if len(fx_instances)>0: - try: - self.lscp_send_single("SET FX_SEND EFFECT %d %d %d %d" % (ls_chan_id,fx_send_id,fx_chain_id,0)) - except zyngine_lscp_error as err: - logging.error(err) - except zyngine_lscp_warning as warn: - logging.warning(warn) - #Save chan info in layer layer.ls_chan_info={ 'chan_id': ls_chan_id, 'fx_chain_id': fx_chain_id, - 'fx_send_id': fx_send_id, - 'fx_instances': fx_instances + 'fx_instances': fx_instances, + 'fx_send_id': None, + 'ls_engine': None } - #Set refresh flag - layer.refresh_flag=True + + def ls_set_preset(self, layer, ls_engine, fpath): + if layer.ls_chan_info: + ls_chan_id=layer.ls_chan_info['chan_id'] + + # Load engine and create FX Send if needed + if ls_engine!=layer.ls_chan_info['ls_engine']: + try: + self.lscp_send_single("LOAD ENGINE %s %d" % (ls_engine,ls_chan_id)) + # Save engine to layer + layer.ls_chan_info['ls_engine']=ls_engine + # Recreate FX send after engine change + if len(layer.ls_chan_info['fx_instances'])>0: + fx_send_id=self.lscp_send_single("CREATE FX_SEND %d %d" % (ls_chan_id,12)) + self.lscp_send_single("SET FX_SEND EFFECT %d %d %d %d" % (ls_chan_id,fx_send_id,layer.ls_chan_info['fx_chain_id'],0)) + # Save FX send to layer + layer.ls_chan_info['fx_send_id']=fx_send_id + except zyngine_lscp_error as err: + logging.error(err) + except zyngine_lscp_warning as warn: + logging.warning(warn) + + # Load instument + try: + self.sock.settimeout(5) + self.lscp_send_single("LOAD INSTRUMENT '%s' 0 %d" % (fpath,ls_chan_id)) + except zyngine_lscp_error as err: + logging.error(err) + except zyngine_lscp_warning as warn: + logging.warning(warn) + self.sock.settimeout(1) def ls_unset_channel(self, layer): if layer.ls_chan_info: - self.lscp_send_single("REMOVE FX_SEND EFFECT %d %d" % (layer.ls_chan_info['chan_id'],layer.ls_chan_info['fx_send_id'])) - self.lscp_send_single("REMOVE SEND_EFFECT_CHAIN %d %d" % (self.ls_audio_device_id,layer.ls_chan_info['fx_chain_id'])) - self.lscp_send_single("REMOVE CHANNEL %d" % layer.ls_chan_info['chan_id']) + chan_id=layer.ls_chan_info['chan_id'] + fx_send_id=layer.ls_chan_info['fx_send_id'] + fx_chain_id=layer.ls_chan_info['fx_chain_id'] + self.lscp_send_single("RESET CHANNEL %d" % chan_id) + # Remove sampler channel + if self.lscp_v1_6_supported: + self.lscp_send_single("REMOVE CHANNEL MIDI_INPUT %d" % chan_id) + if fx_send_id: + self.lscp_send_single("REMOVE FX_SEND EFFECT %d %d" % (chan_id,fx_send_id)) + self.lscp_send_single("REMOVE CHANNEL %d" % chan_id) + + # Remove FX instances from FX chain + fx_len=len(layer.ls_chan_info['fx_instances']) + for i in range(fx_len): + try: + self.lscp_send_single("REMOVE SEND_EFFECT_CHAIN EFFECT %d %d %d" % (self.ls_audio_device_id,fx_chain_id,fx_len-i-1)) + except: + pass + + # Remove FX chain + try: + self.lscp_send_single("REMOVE SEND_EFFECT_CHAIN %d %d" % (self.ls_audio_device_id,fx_chain_id)) + except: + pass + + # Destroy FX instances for name,fx_instance in list(layer.ls_chan_info['fx_instances'].items()): try: self.lscp_send_single("DESTROY EFFECT_INSTANCE %d" % fx_instance['id']) except: pass - #TODO Solve problem "ERR:0:effect still in use" layer.ls_chan_info=None - #****************************************************************************** diff --git a/zyngine/zynthian_layer.py b/zyngine/zynthian_layer.py index dffe9351a..76df827d9 100644 --- a/zyngine/zynthian_layer.py +++ b/zyngine/zynthian_layer.py @@ -75,6 +75,8 @@ def reset(self): def set_midi_chan(self, midi_chan): self.midi_chan=midi_chan self.engine.set_midi_chan(self) + for zctrl in self.controllers_dict.values(): + zctrl.set_midi_chan(midi_chan) def get_midi_chan(self): return self.midi_chan diff --git a/zyngui/zynthian_gui_layer.py b/zyngui/zynthian_gui_layer.py index a9036d1c9..825477be1 100644 --- a/zyngui/zynthian_gui_layer.py +++ b/zyngui/zynthian_gui_layer.py @@ -198,7 +198,11 @@ def load_snapshot(self, fpath): self.layers[-1].restore_snapshot(lss) self.fill_list() self.index=snapshot['index'] - self.select_action(self.index) + if self.list_data[self.index][0] in ('NEW','RESET'): + self.index=0 + zynthian_gui_config.zyngui.show_screen('layer') + else: + self.select_action(self.index) zynthian_gui_config.zyngui.screens['engine'].clean_unused_engines() except Exception as e: logging.error("Invalid snapshot format: %s" % e) diff --git a/zyngui/zynthian_gui_snapshot.py b/zyngui/zynthian_gui_snapshot.py index bfbc7bd73..89037c3b0 100644 --- a/zyngui/zynthian_gui_snapshot.py +++ b/zyngui/zynthian_gui_snapshot.py @@ -78,10 +78,7 @@ def save(self): self.show() def get_new_fpath(self): - try: - n=int(self.list_data[-1][2][3:]) - except: - n=0; + n=max(map(lambda item: int(item[2]) if item[2].isdigit() else 0, self.list_data)) fname='{0:04d}'.format(n+1) + '.zss' fpath=join(self.snapshot_dir,fname) return fpath