From fd7b8383bd1bbc36a348eb22d51c36b230926bca Mon Sep 17 00:00:00 2001 From: ft Date: Sat, 4 Jan 2025 09:01:51 +0000 Subject: [PATCH] Fix sooperlooper per-loop bindings Added ctrl:loop controllers for all per-loop bindable controllers (as previously defined, there may benefit more). But also simple 'ctrl' controllers to always handle the selected loop. The mode of the binding to be made is still changed with selected_loop_cc_binding (on the last page of controls). It is indicated on per-loop bindable controls with (loop number) added to their name. This is updated whenever the loop selection is changed or when selected_loop_cc_binding is set to True. The earlier implementation was incorrect in multiple aspects: 1. It bound ctrl:loopnumbers only, and it only worked when the loop was also already the selected one. This check is now no longer made. 2. Checks for current state checked the selected loop, instead of the targeted loop. The targeted loop (chan) is now set and checked. 2. Selecting a loop with another method than going to the loop selection page and back to another controller page did not work because the UI did not refresh the actual controllers. Fixed this by calling self.state_manager.send_cuia("refresh_screen", ["control"]) (I did not find a more direct way) --- zyngine/zynthian_engine_sooperlooper.py | 70 +++++++++++++++---------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/zyngine/zynthian_engine_sooperlooper.py b/zyngine/zynthian_engine_sooperlooper.py index 43c34a117..1245a3075 100644 --- a/zyngine/zynthian_engine_sooperlooper.py +++ b/zyngine/zynthian_engine_sooperlooper.py @@ -425,12 +425,12 @@ def __init__(self, state_manager=None): # Controller Screens self._ctrl_screens = [ - ['Loop record 1', ['record:0', 'overdub:0', 'multiply:0', 'undo/redo']], - ['Loop record 2', ['replace:0', 'substitute:0', 'insert:0', 'undo/redo']], - ['Loop control', ['trigger:0', 'oneshot:0', 'mute:0', 'pause:0']], - ['Loop time/pitch', ['reverse:0', 'rate', 'stretch_ratio', 'pitch_shift']], + ['Loop record 1', ['record', 'overdub', 'multiply', 'undo/redo']], + ['Loop record 2', ['replace', 'substitute', 'insert', 'undo/redo']], + ['Loop control', ['trigger', 'oneshot', 'mute', 'pause']], + ['Loop time/pitch', ['reverse', 'rate', 'stretch_ratio', 'pitch_shift']], ['Loop levels', ['wet', 'dry', 'feedback', 'selected_loop_num']], - ['Global loop', ['selected_loop_num', 'loop_count', 'prev/next', 'single_pedal:0']], + ['Global loop', ['selected_loop_num', 'loop_count', 'prev/next', 'single_pedal']], ['Global levels', ['rec_thresh', 'input_gain']], ['Global quantize', ['quantize', 'mute_quantized', 'overdub_quantized', 'replace_quantized']], ['Global sync 1', ['sync_source', 'sync', 'playback_sync', 'relative_sync']], @@ -578,26 +578,47 @@ def get_controllers_dict(self, processor): if ctrl[0] in self.SL_LOOP_SEL_PARAM: # Create a zctrl for each loop for i in range(self.MAX_LOOPS): - zctrl = zynthian_controller(self, f"{ctrl[0]}:{i}", ctrl[1]) + name = ctrl[1].get('name') + specs = ctrl[1].copy() + specs['name'] = f"{name} ({i + 1})" + zctrl = zynthian_controller(self, f"{ctrl[0]}:{i}", specs) processor.controllers_dict[zctrl.symbol] = zctrl - else: - zctrl = zynthian_controller(self, ctrl[0], ctrl[1]) - processor.controllers_dict[zctrl.symbol] = zctrl + zctrl = zynthian_controller(self, ctrl[0], ctrl[1]) + processor.controllers_dict[zctrl.symbol] = zctrl return processor.controllers_dict + def adjust_controller_bindings(self): + if self.selected_loop_cc_binding: + self._ctrl_screens[0][1] = [f'record', f'overdub', f'multiply', 'undo/redo'] + self._ctrl_screens[1][1] = [f'replace', f'substitute', f'insert', 'undo/redo'] + self._ctrl_screens[2][1] = [f'trigger', f'oneshot', f'mute', f'pause'] + self._ctrl_screens[3][1][0] = f'reverse' + self._ctrl_screens[5][1][3] = f'single_pedal' + else: + self._ctrl_screens[0][1] = [f'record:{self.selected_loop}', f'overdub:{self.selected_loop}', f'multiply:{self.selected_loop}', 'undo/redo'] + self._ctrl_screens[1][1] = [f'replace:{self.selected_loop}', f'substitute:{self.selected_loop}', f'insert:{self.selected_loop}', 'undo/redo'] + self._ctrl_screens[2][1] = [f'trigger:{self.selected_loop}', f'oneshot:{self.selected_loop}', f'mute:{self.selected_loop}', f'pause:{self.selected_loop}'] + self._ctrl_screens[3][1][0] = f'reverse:{self.selected_loop}' + self._ctrl_screens[5][1][3] = f'single_pedal:{self.selected_loop}' + return + def send_controller_value(self, zctrl): if zctrl.symbol == "selected_loop_cc": self.selected_loop_cc_binding = zctrl.value != 0 + self.adjust_controller_bindings() + try: + processor = self.processors[0] + except IndexError: + return + processor.refresh_controllers() + self.state_manager.send_cuia("refresh_screen", ["control"]) return if ":" in zctrl.symbol: symbol, chan = zctrl.symbol.split(":") - if self.selected_loop_cc_binding: - if int(chan) != self.selected_loop: - return - chan = -3 + chan = int(chan) else: symbol = zctrl.symbol - chan = -3 + chan = self.selected_loop if self.osc_server is None or symbol in ['oneshot', 'trigger'] and zctrl.value == 0: # Ignore off signals return @@ -629,13 +650,13 @@ def send_controller_value(self, zctrl): elif self.pedal_taps == 1: self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'pause')) # Single tap - elif self.state[self.selected_loop] in (SL_STATE_UNKNOWN, SL_STATE_OFF, SL_STATE_OFF_MUTED): + elif self.state[chan] in (SL_STATE_UNKNOWN, SL_STATE_OFF, SL_STATE_OFF_MUTED): self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'record')) - elif self.state[self.selected_loop] == SL_STATE_RECORDING: + elif self.state[chan] == SL_STATE_RECORDING: self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'record')) - elif self.state[self.selected_loop] in (SL_STATE_PLAYING, SL_STATE_OVERDUBBING): + elif self.state[chan] in (SL_STATE_PLAYING, SL_STATE_OVERDUBBING): self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'overdub')) - elif self.state[self.selected_loop] == SL_STATE_PAUSED: + elif self.state[chan] == SL_STATE_PAUSED: self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'trigger')) # Pedal release: so check loop state, pedal press duration, etc. else: @@ -654,7 +675,7 @@ def send_controller_value(self, zctrl): elif zctrl.is_toggle: # Use is_toggle to indicate the SL function is a toggle, i.e. press to engage, press to release - if symbol == 'record' and zctrl.value == 0 and self.state[self.selected_loop] == SL_STATE_REC_STARTING: + if symbol == 'record' and zctrl.value == 0 and self.state[chan] == SL_STATE_REC_STARTING: # TODO: Implement better toggle of pending state self.osc_server.send(self.osc_target, f'/sl/{chan}/hit', ('s', 'undo')) return @@ -832,7 +853,7 @@ def update_state(self, loop): def select_loop(self, loop, send=False): try: processor = self.processors[0] - except: + except IndexError: return if loop < 0 or loop >= self.loop_count: return # TODO: Handle -1 == all loops @@ -844,16 +865,11 @@ def select_loop(self, loop, send=False): self.update_state() """ processor.controllers_dict['selected_loop_num'].set_value(loop + 1, False) + self.adjust_controller_bindings() if send and self.osc_server: self.osc_server.send(self.osc_target, '/set', ('s', 'selected_loop_num'), ('f', self.selected_loop)) - - self._ctrl_screens[0][1] = [f'record:{self.selected_loop}', f'overdub:{self.selected_loop}', f'multiply:{self.selected_loop}', 'undo/redo'] - self._ctrl_screens[1][1] = [f'replace:{self.selected_loop}', f'substitute:{self.selected_loop}', f'insert:{self.selected_loop}', 'undo/redo'] - self._ctrl_screens[2][1] = [f'trigger:{self.selected_loop}', f'oneshot:{self.selected_loop}', f'mute:{self.selected_loop}', f'pause:{self.selected_loop}'] - self._ctrl_screens[3][1][0] = f'reverse:{self.selected_loop}' - self._ctrl_screens[5][1][3] = f'single_pedal:{self.selected_loop}' processor.refresh_controllers() - + self.state_manager.send_cuia("refresh_screen", ["control"]) zynsigman.send_queued(zynsigman.S_GUI, SS_GUI_CONTROL_MODE, mode='control') def prev_loop(self):