From 82d5fd610f9086fc8f3575471cf4765f31fe515f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20Andr=C3=A9?= Date: Wed, 9 Jun 2021 01:55:43 +0200 Subject: [PATCH] refactor: add Action class --- README.md | 2 +- setup.cfg | 2 + src/clyphx/actions/arsenal.py | 2 +- src/clyphx/actions/clip.py | 38 +- src/clyphx/actions/clip_env_capture.py | 4 +- src/clyphx/actions/clip_notes.py | 8 +- src/clyphx/actions/consts.py | 12 +- src/clyphx/actions/control_surface.py | 12 +- src/clyphx/actions/device.py | 49 +-- src/clyphx/actions/device_looper.py | 5 + src/clyphx/actions/dr.py | 93 ++--- src/clyphx/actions/global_.py | 474 +++++++++++++------------ src/clyphx/actions/push.py | 4 +- src/clyphx/actions/pxt_live.py | 4 +- src/clyphx/actions/scene.py | 18 +- src/clyphx/actions/snap.py | 80 ++--- src/clyphx/actions/track.py | 18 +- src/clyphx/clyphx.py | 51 ++- src/clyphx/consts.py | 35 +- src/clyphx/core/action.py | 67 ++++ src/clyphx/core/exceptions.py | 7 +- src/clyphx/core/legacy.py | 5 +- src/clyphx/core/live.py | 25 +- src/clyphx/core/models.py | 5 + src/clyphx/core/parse.py | 56 ++- src/clyphx/core/utils.py | 4 +- src/clyphx/core/xcomponent.py | 12 +- src/clyphx/cs_linker.py | 5 +- src/clyphx/instant_doc.py | 2 +- src/clyphx/macrobat/parameter_racks.py | 2 +- src/clyphx/triggers/track.py | 6 +- src/clyphx/user_actions.py | 2 +- src/clyphx/user_config.py | 2 +- tests/test_parsing.py | 11 +- 34 files changed, 654 insertions(+), 468 deletions(-) create mode 100644 src/clyphx/core/action.py diff --git a/README.md b/README.md index d4f3b2d..57d4d73 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _Action_ or a list of _Actions_. **ClyphX** also includes: -- A component called **Macrobat** that adds new functionality to racks in Live +- A component called **[Macrobat](./docs/macrobat_manual.md)** that adds new functionality to racks in Live such as the ability to control track mixer parameters or send MIDI messages from Rack Macros. diff --git a/setup.cfg b/setup.cfg index ffb8af4..8236b95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,9 @@ warn_no_return = false ignore = E221, # multiple spaces before operator E241, # multiple spaces after ':' + E251, # unexpected spaces around keyword / parameter equals E303, # too many blank lines (2) + E731, # do not assign a lambda expression, use a def W503, # line break before binary operator exclude = .git, diff --git a/src/clyphx/actions/arsenal.py b/src/clyphx/actions/arsenal.py index 32a2604..426223d 100644 --- a/src/clyphx/actions/arsenal.py +++ b/src/clyphx/actions/arsenal.py @@ -34,7 +34,7 @@ from _NKFW2.ScaleSettingsComponent import SEQ_OFFSET, FOURTHS_OFFSET, OFFSET_NAMES S_TYPES = [s.name.upper() for s in SCALE_TYPES] # type: List[Text] O_NAMES = [o.upper() for o in OFFSET_NAMES] # type: List[Text] -except: +except Exception: pass log = logging.getLogger(__name__) diff --git a/src/clyphx/actions/clip.py b/src/clyphx/actions/clip.py index f8447b8..9ffeadf 100644 --- a/src/clyphx/actions/clip.py +++ b/src/clyphx/actions/clip.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from typing import Any, Optional, Text, Dict, List, Tuple from ..core.live import DeviceParameter, Track - from ..core.legacy import _DispatchCommand + from ..core.legacy import _DispatchCommand, _SingleDispatch from ..core.xcomponent import XComponent from ..core.live import Clip, Conversions @@ -52,8 +52,7 @@ def dispatch_actions(self, cmd): for scmd in cmd: # TODO: compare with dispatch_device_actions if scmd.track in self._parent.song().tracks: - _args = scmd.track, scmd.action_name, scmd.args - action = self.get_clip_to_operate_on(*_args) + action = self.get_clip_to_operate_on(scmd) clip_args = None if action[0]: if len(action) > 1: @@ -63,41 +62,42 @@ def dispatch_actions(self, cmd): func(self, action[0], scmd.track, scmd.xclip, clip_args.replace(clip_args.split()[0], '')) elif clip_args and clip_args.split()[0].startswith('NOTES'): - args = [a.strip() for a in cmd.args.split() if a.strip()] - self.dispatch_clip_note_action(action[0], args) + # args = [a.strip() for a in cmd.args.split() if a.strip()] + self.dispatch_clip_note_action(action[0], scmd.args) elif cmd.action_name.startswith('CLIP'): - self.set_clip_on_off(action[0], scmd.track, scmd.xclip, cmd.args) + self.set_clip_on_off(action[0], scmd.track, scmd.xclip, scmd.args) - def get_clip_to_operate_on(self, track, action_name, args): - # type: (Track, Text, Text) -> Tuple[Optional[Clip], Text] + def get_clip_to_operate_on(self, scmd): + # type: (_SingleDispatch) -> Tuple[Optional[Clip], Text] '''Get clip to operate on and action to perform with args.''' + args = scmd.args try: # TODO: add args to parsing - parsed = self._parent.parse_obj('clip', action_name) + parsed = self._parent.parse_obj('clip', scmd.action_name) except Exception: try: - parsed, args = self._parse_split_name(action_name, args) + parsed, args = self._parse_split_name(scmd.action_name, args) except Exception as e: log.error('Failed to parse clip: %r', e) return clip = None if 'name' in parsed: # CLIP"Name" - clip = self.get_clip_by_name(track, parsed['name']) + clip = self.get_clip_by_name(scmd.track, parsed['name']) elif 'sel' in parsed: # CLIPSEL if self.application().view.is_view_visible('Arranger'): clip = self.song().view.detail_clip else: slot_idx = None if not parsed: # CLIP - if track.playing_slot_index >= 0: - slot_idx = track.playing_slot_index + if scmd.track.playing_slot_index >= 0: + slot_idx = scmd.track.playing_slot_index elif 'pos' in parsed: # CLIPx slot_idx = int(parsed['pos']) - 1 slot_idx = self.sel_scene if slot_idx is None else slot_idx - if track.clip_slots[slot_idx].has_clip: - clip = track.clip_slots[slot_idx].clip + if scmd.track.clip_slots[slot_idx].has_clip: + clip = scmd.track.clip_slots[slot_idx].clip return clip, args @staticmethod @@ -386,7 +386,7 @@ def insert_envelope(self, clip, track, xclip, args): if 0 <= min_factor and max_factor < 101 and min_factor < max_factor: env_range = ((min_factor / 100.0) * param.max, (max_factor / 100.0) * param.max) - except: + except Exception: pass self.song().view.detail_clip = clip clip.view.show_envelope() @@ -519,14 +519,14 @@ def quantize(self, clip, track, xclip, args): strength = float(arg_array[1 + array_offset]) / 100.0 if strength > 1.0 or strength < 0.0: strength = 1.0 - except: + except Exception: strength = 1.0 if len(arg_array) > 2 + array_offset: try: swing_to_apply = float(arg_array[2 + array_offset]) / 100.0 if swing_to_apply > 1.0 or swing_to_apply < 0.0: swing_to_apply = 0.0 - except: + except Exception: swing_to_apply = 0.0 self.song().swing_amount = swing_to_apply # apply standard qntz to all @@ -576,7 +576,7 @@ def chop_clip(self, clip, track, xclip, args): if args: try: num_chops = int(args) - except: + except Exception: pass slot_index = list(track.clip_slots).index(clip.canonical_parent) current_start = clip.start_marker diff --git a/src/clyphx/actions/clip_env_capture.py b/src/clyphx/actions/clip_env_capture.py index 8574dcc..9845eb3 100644 --- a/src/clyphx/actions/clip_env_capture.py +++ b/src/clyphx/actions/clip_env_capture.py @@ -106,13 +106,13 @@ def _get_device_range(args, track): s, e = dev_args.split('-') start = int(s) - 1 end = int(e) - except: + except Exception: start, end = 0, 1 else: try: start = int(dev_args) - 1 end = start + 1 - except: + except Exception: pass if 0 <= start and end <= len(track.devices) and start <= end: return (start, end) diff --git a/src/clyphx/actions/clip_notes.py b/src/clyphx/actions/clip_notes.py index 25c3e70..997362d 100644 --- a/src/clyphx/actions/clip_notes.py +++ b/src/clyphx/actions/clip_notes.py @@ -92,7 +92,7 @@ def get_pos_range(clip, string): user_range = string.split('-') try: start = float(user_range[0].replace('@', '')) - except: + except Exception: pass else: if start >= 0.0: @@ -100,7 +100,7 @@ def get_pos_range(clip, string): if len(user_range) > 1: try: pos_range = (start, float(user_range[1])) - except: + except Exception: pass return pos_range or (clip.loop_start, clip.loop_end) @@ -113,7 +113,7 @@ def get_note_range(self, string): if len(string) > 1: try: note_range = pitch_range(string) - except: + except Exception: try: start_note = Pitch.first_note(string) note_range = (start_note, start_note + 1) @@ -288,7 +288,7 @@ def do_note_velo_adjustment(self, data): for n in data.notes_to_edit: try: edited.append((n[0], n[1], n[2], float(arg), n[4])) - except: + except Exception: pass return edited diff --git a/src/clyphx/actions/consts.py b/src/clyphx/actions/consts.py index 99ef33a..6ada5eb 100644 --- a/src/clyphx/actions/consts.py +++ b/src/clyphx/actions/consts.py @@ -155,7 +155,7 @@ TOSIMP = XClipActions.to_simpler, ) # type: Dict[Text, Callable] -NOTES_ACTIONS = dict([ +NOTES_ACTIONS = dict(( ('REV', NotesMixin.do_note_reverse), ('INV', NotesMixin.do_note_invert), ('COMP', NotesMixin.do_note_compress), @@ -172,8 +172,7 @@ ('OFF', NotesMixin.set_notes_on_off), (None, NotesMixin.set_notes_on_off), ('', NotesMixin.set_notes_on_off), -]) # type: Dict[Text, Callable[[Clip, Any], List[None]]] - +)) # type: Dict[Text, Callable[[Clip, Any], List[None]]] DEVICE_ACTIONS = dict( CSEL = XDeviceActions.adjust_selected_chain, @@ -209,6 +208,13 @@ STOP = XDeviceActions.set_looper_state, ) # type: Dict[Text, Callable[[XDeviceActions, Optional[Text]], None]] +CHAIN_ACTIONS = dict( + MUTE = XDeviceActions.mute_chain, + SOLO = XDeviceActions.solo_chain, + VOL = XDeviceActions.adjust_chain_volume, + PAN = XDeviceActions.adjust_chain_panning, +) + DR_ACTIONS = dict( SCROLL = XDrActions.scroll_selector, UNMUTE = XDrActions.unmute_all, diff --git a/src/clyphx/actions/control_surface.py b/src/clyphx/actions/control_surface.py index fdcf5be..0b38f4f 100644 --- a/src/clyphx/actions/control_surface.py +++ b/src/clyphx/actions/control_surface.py @@ -245,7 +245,7 @@ def _get_script_to_operate_on(self, script_info): if v['name'] == script_spec: script = k break - except: + except Exception: script = None return script @@ -289,7 +289,7 @@ def handle_track_action(self, script_key, mixer, xclip, ident, args): else: track_start = int(track_range) - 1 track_end = track_start + 1 - except: + except Exception: return if (0 <= track_start and track_start < track_end and @@ -333,7 +333,7 @@ def handle_track_bank(self, script_key, xclip, ident, mixer, session, args): offset = int(args) if 0 <= (offset + t_offset) < len(tracks): new_offset = offset + t_offset - except: + except Exception: return if new_offset >= 0: @@ -369,7 +369,7 @@ def handle_session_offset(self, script_key, session, args): if new_track == -1 or new_scene == -1: return session.set_offsets(new_track, new_scene) - except: + except Exception: pass def _parse_ring_spec(self, spec_id, arg_string, default_index, list_to_search): @@ -477,7 +477,7 @@ def on_selected_track_changed(self): if 0 <= centered_id < len(self.song().visible_tracks): new_trk_id = centered_id session.set_offsets(new_trk_id, s_offset) - except: + except Exception: pass def on_selected_scene_changed(self): @@ -509,7 +509,7 @@ def on_selected_scene_changed(self): if 0 <= centered_id < len(self.song().scenes): new_scn_id = centered_id session.set_offsets(t_offset, new_scn_id) - except: + except Exception: pass diff --git a/src/clyphx/actions/device.py b/src/clyphx/actions/device.py index 27c2c4b..cf27db0 100644 --- a/src/clyphx/actions/device.py +++ b/src/clyphx/actions/device.py @@ -119,7 +119,7 @@ def adjust_selected_chain(self, device, track, xclip, args): else: try: new_index = int(args) - 1 - except: + except Exception: new_index = list(device.chains).index(device.view.selected_chain) if 0 <= new_index < len(device.chains): device.view.selected_chain = device.chains[new_index] @@ -187,9 +187,9 @@ def get_chain_selector(device): # type: (Device) -> Optional[DeviceParameter] '''Get rack chain selector param.''' if device.class_name.endswith('GroupDevice'): - for parameter in device.parameters: - if str(parameter.original_name) == 'Chain Selector': - return parameter + for param in device.parameters: + if str(param.original_name) == 'Chain Selector': + return param return None @staticmethod @@ -219,17 +219,17 @@ def get_bank_param(device, param, bank='B0'): def dispatch_chain_action(self, device, args): # type: (Device, List[Text]) -> None '''Handle actions related to device chains.''' + from .consts import CHAIN_ACTIONS + if not (self._parent._can_have_nested_devices and device.can_have_chains and device.chains): raise TypeError('This device does not support chain actions.') try: chain_num, action, value = args[0:3] + chain_num = chain_num.replace('CHAIN', '').replace('CH', '') + chain = device.chains[int(chain_num) - 1] - chain = device.chains[int(chain_num.replace('CHAIN', '')) - 1] - - if action not in {'MUTE', 'SOLO', 'VOL', 'PAN'}: - raise InvalidAction('Invalid device chain action: {}'.format(action)) - elif action in {'VOL', 'PAN'} and device.class_name.startswith('Midi'): + if action in {'VOL', 'PAN'} and device.class_name.startswith('Midi'): raise InvalidAction('Invalid MIDI device chain action: {}'.format(action)) except Exception as e: log.error("Failed to parse chain action args '%s': %r", args, e) @@ -237,16 +237,23 @@ def dispatch_chain_action(self, device, args): chain = device.view.selected_chain if chain: - func, obj, param = dict( - # FIXME - MUTE = (switch, chain, 'mute'), - SOLO = (switch, chain, 'solo'), - VOL = (self._adjust_param, chain.mixer_device, 'volume'), - PAN = (self._adjust_param, chain.mixer_device, 'panning'), - ) - func(obj, param, value) - - # FIXME: - def _adjust_param(self, obj, param, value): - self.adjust_param(getattr(obj, param), value) + try: + func = CHAIN_ACTIONS[action] + except KeyError: + raise InvalidAction('Invalid device chain action: {}'.format(action)) + else: + func(chain, value) + + def mute_chain(self, chain, value): + switch(chain, 'mute', value) + + def solo_chain(self, chain, value): + switch(chain, 'solo', value) + + def adjust_chain_volume(self, chain, value): + self.adjust_param(chain.mixer_device.volume, value) + + def adjust_chain_panning(self, chain, value): + self.adjust_param(chain.mixer_device.panning, value) + # endregion diff --git a/src/clyphx/actions/device_looper.py b/src/clyphx/actions/device_looper.py index ee2c4cc..6c73d92 100644 --- a/src/clyphx/actions/device_looper.py +++ b/src/clyphx/actions/device_looper.py @@ -1,9 +1,14 @@ from __future__ import absolute_import, unicode_literals from builtins import object +from typing import TYPE_CHECKING from ..consts import LOOPER_STATES, switch from ..core.exceptions import InvalidParam +if TYPE_CHECKING: + from typing import Text + from ..core.legacy import _DispatchCommand + # TODO: turn into a component? class LooperMixin(object): diff --git a/src/clyphx/actions/dr.py b/src/clyphx/actions/dr.py index 4c79ee8..90bda6c 100644 --- a/src/clyphx/actions/dr.py +++ b/src/clyphx/actions/dr.py @@ -20,7 +20,6 @@ if TYPE_CHECKING: from typing import Any, Optional, Callable, Iterable, Sequence, Text - from ..core.legacy import _DispatchCommand from ..core.live import Track, Clip, Device from ..core.xcomponent import XComponent @@ -36,33 +35,34 @@ class XDrActions(XComponent): def dispatch_dr_actions(self, cmd): if cmd.args: + args = [a.strip() for a in cmd.args.split() if a.strip()] for scmd in cmd: - self.dispatch_dr_action(scmd.track, scmd.xclip, scmd.ident, scmd.args) + # self.dispatch_dr_action(scmd.track, scmd.args) + self.dispatch_dr_action(scmd.track, *args) - def dispatch_dr_action(self, track, xclip, ident, args): + def dispatch_dr_action(self, track, action, *args): # type: (Track, Clip, Text, Text) -> None from .consts import DR_ACTIONS - arg_action = DR_ACTIONS.get(args.split()[0].upper()) - if arg_action: - action = partial(arg_action, self) # type: Callable - elif 'PAD' in args: - action = self.dispatch_pad_action + func = DR_ACTIONS.get(action.upper()) + if func: + method = partial(func, self) # type: Callable + elif 'PAD' in args[0]: + method = self.dispatch_pad_action else: return # get dr to operate on for device in track.devices: if device.can_have_drum_pads: - action(device, args) + method(device, *args) break - def scroll_selector(self, dr, args): + def scroll_selector(self, dr, factor): # type: (Device, Text) -> None '''Scroll Drum Rack selector up/down.''' - args = args.replace('SCROLL', '').strip() - if args.startswith(('<', '>')): - factor = self.get_adjustment_factor(args) + if factor.startswith(('<', '>')): + factor = self.get_adjustment_factor(factor) pos = dr.view.drum_pads_scroll_position if factor > 0: if pos < MAX_SCROLL_POS - factor: @@ -76,89 +76,90 @@ def scroll_selector(self, dr, args): value = 0 dr.view.drum_pads_scroll_position = value - def unmute_all(self, dr, args): - # type: (Device, None) -> None + def unmute_all(self, dr): + # type: (Device) -> None '''Unmute all pads in the Drum Rack.''' for pad in dr.drum_pads: pad.mute = False - def unsolo_all(self, dr, args): - # type: (Device, None) -> None + def unsolo_all(self, dr): + # type: (Device) -> None '''Unsolo all pads in the Drum Rack.''' for pad in dr.drum_pads: pad.solo = False # region PAD ACTIONS - def dispatch_pad_action(self, dr, track, xclip, ident, _args): - # type: (Any, None, None, None, Text) -> None + def dispatch_pad_action(self, dr, pad, *args): + # type: (Any, Text) -> None '''Dispatches pad-based actions.''' from .consts import PAD_ACTIONS - args = _args.split() - if len(args) > 1: - pads = self._get_pads_to_operate_on(dr, args[0].replace('PAD', '').strip()) + if args: + pads = self._get_pads_to_operate_on(dr, pad.replace('PAD', '').strip()) if pads: - action = args[1].upper() - action_arg = args[2] if len(args) > 2 else None + action = args[0].upper() + action_arg = args[1] if len(args) > 1 else None if action in PAD_ACTIONS: PAD_ACTIONS[action](self, pads, action_arg) elif action == 'SEL': dr.view.selected_drum_pad = pads[-1] - elif 'SEND' in action and action_arg and len(args) > 3: - self._adjust_pad_send(pads, args[3], action_arg) + elif 'SEND' in action and action_arg and len(args) > 2: + self._adjust_pad_send(pads, args[2], action_arg) - def _mute_pads(self, pads, action_arg): + def _mute_pads(self, pads, arg): # type: (Iterable[Any], Optional[Text]) -> None '''Toggles or turns on/off pad mute.''' for pad in pads: - switch(pad, 'mute', action_arg) + switch(pad, 'mute', arg) - def _solo_pads(self, pads, action_arg): + def _solo_pads(self, pads, arg): # type: (Iterable[Any], Optional[Text]) -> None '''Toggles or turns on/off pad solo.''' for pad in pads: - switch(pad, 'solo', action_arg) + switch(pad, 'solo', arg) - def _adjust_pad_volume(self, pads, action_arg): + def _adjust_pad_volume(self, pads, arg): # type: (Sequence[Any], Text) -> None '''Adjust/set pad volume.''' for pad in pads: if pad.chains: param = pad.chains[0].mixer_device.volume - self.adjust_param(param, action_arg) + self.adjust_param(param, arg) - def _adjust_pad_pan(self, pads, action_arg): + def _adjust_pad_pan(self, pads, arg): # type: (Sequence[Any], Text) -> None '''Adjust/set pad pan.''' for pad in pads: if pad.chains: param = pad.chains[0].mixer_device.panning - self.adjust_param(param, action_arg) + self.adjust_param(param, arg) - def _adjust_pad_send(self, pads, action_arg, send): + def _adjust_pad_send(self, pads, arg, send): # type: (Sequence[Any], Text, Text) -> None '''Adjust/set pad send.''' try: for pad in pads: if pad.chains: param = pad.chains[0].mixer_device.sends[ord(send) - 65] - self.adjust_param(param, action_arg) - except: + self.adjust_param(param, arg) + except Exception: pass @staticmethod - def _get_pads_to_operate_on(dr, pads): + def _get_pads_to_operate_on(dr, arg): # type: (Any, Text) -> Sequence[Any] '''Get the Drum Rack pad or pads to operate on.''' - pads_to_operate_on = [dr.view.selected_drum_pad] - if pads == 'ALL': - pads_to_operate_on = dr.visible_drum_pads - elif pads: + pads = None + if arg == 'ALL': + pads = dr.visible_drum_pads + elif arg: try: - index = int(pads) - 1 + index = int(arg) - 1 if 0 <= index < 16: - pads_to_operate_on = [dr.visible_drum_pads[index]] - except: + pads = [dr.visible_drum_pads[index]] + except Exception: pass - return pads_to_operate_on + if pads is None: + return [dr.view.selected_drum_pad] + return pads # endregion diff --git a/src/clyphx/actions/global_.py b/src/clyphx/actions/global_.py index 8489257..2ad46db 100644 --- a/src/clyphx/actions/global_.py +++ b/src/clyphx/actions/global_.py @@ -30,7 +30,7 @@ from ..core.legacy import _SingleDispatch from ..core.xcomponent import XComponent -from ..core.live import Application, Clip, DeviceType, get_random_int +from ..core.live import Application, Clip, DeviceType from ..consts import KEYWORDS, switch from ..consts import (AUDIO_DEVS, MIDI_DEVS, INS_DEVS, GQ_STATES, REPEAT_STATES, RQ_STATES, @@ -77,73 +77,71 @@ def dispatch_action(self, cmd): # type: (_SingleDispatch) -> None from .consts import GLOBAL_ACTIONS - if cmd.action_name == 'SCENE': - # TODO: - # action = SCENE_ACTIONS[] - pass - else: - action = GLOBAL_ACTIONS[cmd.action_name] - action(self, cmd.track, cmd.xclip, cmd.args) - - def make_instant_mapping_docs(self, track, xclip, args): - # type: (None, Clip, Text) -> None + # if cmd.action_name == 'SCENE' and False: + # # TODO: + # # action = SCENE_ACTIONS[] + # pass + # else: + # action = GLOBAL_ACTIONS[cmd.action_name] + action = GLOBAL_ACTIONS[cmd.action_name] + args = cmd.args.strip().split() if cmd.args and cmd.args.strip() else () + action(self, cmd.track, cmd.xclip, *args) + + def make_instant_mapping_docs(self, track, xclip, *args): + # type: (None, Clip, None) -> None from ..instant_doc import InstantMappingMakeDoc InstantMappingMakeDoc() if isinstance(xclip, Clip): xclip.name = str(xclip.name).upper().replace('MAKE_DEV_DOC', 'Doc saved') - def send_midi_message(self, track, xclip, args): + def send_midi_message(self, track, xclip, *args): # type: (None, None, Text) -> None '''Send formatted NOTE/CC/PC message or raw MIDI message.''' message = [] - if args: - byte_array = args.split() - if len(byte_array) >= 3 and byte_array[0] in MIDI_STATUS: - try: - data_bytes = list(map(int, byte_array[1:])) - except: - pass - else: - if 1 <= data_bytes[0] < 17: - message = [MIDI_STATUS[byte_array[0]] + data_bytes[0] - 1] - for byte in data_bytes[1:]: - if 0 <= byte < 128: - message.append(byte) - if ((byte_array[0] != 'PC' and len(message) != 3) or - (byte_array[0] == 'PC' and len(message) != 2)): - return - elif len(byte_array) >= 2: - try: - message = list(map(int, byte_array)) - except: - pass - if message: - try: - self._parent._send_midi(tuple(message)) - # send matching note off for note messages - if byte_array[0] == 'NOTE': - message[-1] = 0 - self._parent.schedule_message( - 1, partial(self._parent._send_midi, tuple(message)) - ) - except: - pass - def do_variable_assignment(self, track, xclip, args): + if len(args) >= 3 and args[0] in MIDI_STATUS: + try: + data_bytes = list(map(int, args[1:])) + except Exception: + pass + else: + if 1 <= data_bytes[0] < 17: + message = [MIDI_STATUS[args[0]] + data_bytes[0] - 1] + for byte in data_bytes[1:]: + if 0 <= byte < 128: + message.append(byte) + if ((args[0] != 'PC' and len(message) != 3) or + (args[0] == 'PC' and len(message) != 2)): + return + elif len(args) >= 2: + try: + message = list(map(int, args)) + except Exception: + pass + + if message: + try: + self._parent._send_midi(tuple(message)) + # send matching note off for note messages + if args[0] == 'NOTE': + message[-1] = 0 + self._parent.schedule_message( + 1, partial(self._parent._send_midi, tuple(message)) + ) + except Exception: + pass + + def do_variable_assignment(self, track, xclip, name, start, length): # type: (None, None, Text) -> None '''Creates numbered variables for the name given in args from the offset given in args and in the quantity given in args. ''' - args = args.strip() - arg_array = args.split() - if len(arg_array) == 3: - try: - start = int(arg_array[1]) - length = int(arg_array[2]) - for i in range(length): - self._parent._user_variables[arg_array[0] + str(i + 1)] = str(i + start) - except: - pass + try: + start = int(start) + for i in range(int(length)): + self._parent._user_variables['{}{}'.format(name, i + 1)] = str(i + start) + except Exception: + pass # region TRACKS @@ -152,7 +150,7 @@ def create_audio_track(self, track, xclip, value=None): '''Creates audio track at end of track list or at the specified index. ''' - if value and value.strip(): + if value: try: index = int(value) - 1 if 0 <= index < len(self.song().tracks): @@ -167,7 +165,7 @@ def create_midi_track(self, track, xclip, value=None): '''Creates MIDI track at end of track list or at the specified index. ''' - if value and value.strip(): + if value: try: index = int(value) - 1 if 0 <= index < len(self.song().tracks): @@ -177,19 +175,19 @@ def create_midi_track(self, track, xclip, value=None): else: self.song().create_midi_track(-1) - def create_return_track(self, track, xclip, value=None): + def create_return_track(self, track, xclip, *args): # type: (None, None, None) -> None '''Creates return track at end of return list.''' self.song().create_return_track() - def insert_and_configure_audio_track(self, track, xclip, value=None): + def insert_and_configure_audio_track(self, track, xclip, *args): # type: (None, None, None) -> None '''Inserts an audio track next to the selected track routed from the selected track and armed. ''' self._insert_and_configure_track(is_midi=False) - def insert_and_configure_midi_track(self, track, xclip, value=None): + def insert_and_configure_midi_track(self, track, xclip, *args): # type: (None, None, None) -> None '''Inserts a midi track next to the selected track routed from the selected track and armed. @@ -217,12 +215,12 @@ def _insert_and_configure_track(self, is_midi=False): new_track.name = 'From {}'.format(sel_track.name) new_track.current_input_routing = sel_track.name new_track.arm = True - except: + except Exception: pass # endregion - def swap_device_preset(self, track, xclip, args): + def swap_device_preset(self, track, xclip, *args): # type: (Track, None, Text) -> None '''Activates swapping for the selected device or swaps out the preset for the given device with the given preset or navigates @@ -237,7 +235,7 @@ def swap_device_preset(self, track, xclip, args): self.application().view.toggle_browse() tag_target = None dev_name = device.class_display_name - args = args.strip() + if device.type == DeviceType.audio_effect: tag_target = self.application().browser.audio_effects elif device.type == DeviceType.midi_effect: @@ -247,14 +245,14 @@ def swap_device_preset(self, track, xclip, args): if tag_target: for dev in tag_target.children: if dev.name == dev_name: - self._handle_swapping(device, dev, args) + self._handle_swapping(device, dev, *args) break - def _handle_swapping(self, device, browser_item, args): + def _handle_swapping(self, device, browser_item, arg): # type: (Device, Any, Text) -> None dev_items = self._create_device_items(browser_item, []) - if args in ('<', '>'): - factor = self.get_adjustment_factor(args) + if arg in ('<', '>'): + factor = self.get_adjustment_factor(arg) index = self._get_current_preset_index(device, dev_items) new_index = index + factor if new_index > len(dev_items) - 1: @@ -263,9 +261,10 @@ def _handle_swapping(self, device, browser_item, args): new_index = -1 self._load_preset(dev_items[new_index]) else: - args += '.ADG' if device.can_have_chains else '.ADV' + ext = 'ADG' if device.can_have_chains else 'ADV' + arg = '{}.{}'.format(arg.upper(), ext) for item in dev_items: - if item.name.upper() == args: + if item.name.upper() == arg: self._load_preset(item) break @@ -301,50 +300,57 @@ def _create_device_items(self, device, item_array): item_array.append(item) return item_array - def load_device(self, track, xclip, args): - # type: (None, None, Text) -> None - '''Loads one of Live's built-in devices onto the selected Track. - ''' - # XXX: using a similar method for loading plugins doesn't seem to work! - args = args.strip() - - if args in AUDIO_DEVS: + def get_device(self, arg): + if arg in AUDIO_DEVS: tag_target = self.application().browser.audio_effects - name = AUDIO_DEVS[args] - elif args in MIDI_DEVS: + name = AUDIO_DEVS[arg] + elif arg in MIDI_DEVS: tag_target = self.application().browser.midi_effects - name = MIDI_DEVS[args] - elif args in INS_DEVS: + name = MIDI_DEVS[arg] + elif arg in INS_DEVS: tag_target = self.application().browser.instruments - name = INS_DEVS[args] + name = INS_DEVS[arg] else: - log.warning("Device '%s' not found", args) + log.warning("Device '%s' not found", arg) return for dev in tag_target.children: if dev.name == name: - self.application().browser.load_item(dev) - break + return dev + else: + log.warning("Device '%s' not found", name) - def load_m4l(self, track, xclip, args): - # type: (None, None, Text) -> None - '''Loads M4L device onto the selected Track. The .amxd should be - omitted by the user. - ''' - args = '{}.AMXD'.format(args.strip()) - found_dev = False + def get_m4l_device(self, arg): + if not arg.endswith('.AMXD'): + arg = '{}.AMXD'.format(arg) for m in self.application().browser.max_for_live.children: for device in m.children: if device.is_folder: for dev in device.children: - if dev.name.upper() == args: - found_dev = dev - break - elif device.name.upper() == args: - found_dev = device - if found_dev: - self.application().browser.load_item(found_dev) - break + if dev.name.upper() == arg: + return dev + elif device.name.upper() == arg: + return device + else: + log.warning('M4L device not found: %s', arg) + + def load_device(self, track, xclip, arg): + # type: (None, None, Text) -> None + '''Loads one of Live's built-in devices onto the selected Track. + ''' + # XXX: using a similar method for loading plugins doesn't seem to work + dev = self.get_device(arg) + if dev is not None: + self.application().browser.load_item(dev) + + def load_m4l(self, track, xclip, arg): + # type: (None, None, Text) -> None + '''Loads M4L device onto the selected Track. The .amxd should be + omitted by the user. + ''' + dev = self.get_m4l_device(arg.upper()) + if dev is not None: + self.application().browser.load_item(dev) def set_session_record(self, track, xclip, value=None): # type: (None, None, Optional[Text]) -> None @@ -369,7 +375,7 @@ def trigger_session_record(self, track, xclip, value=None): bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator try: length = float(value) * bar - except: + except Exception: length = bar self.song().trigger_session_record(length) @@ -378,10 +384,7 @@ def _track_has_empty_slot(self, track, start): '''Returns whether the given track has an empty slot existing after the starting slot index. ''' - for s in track.clip_slots[start:]: - if not s.has_clip: - return True - return False + return any(not s.has_clip for s in track.clip_slots[start:]) def set_session_automation_record(self, track, xclip, value=None): # type: (None, None, Optional[Text]) -> None @@ -466,6 +469,12 @@ def set_redo(self, track, xclip, value=None): # region NAVIGATION + def _move_nav(self, direction): + # type: (int) -> None + self.application().view.scroll_view( + Application.View.NavDirection(direction), '', False + ) + def move_up(self, track, xclip, value=None): # type: (None, None, None) -> None '''Scroll up.''' @@ -486,11 +495,23 @@ def move_right(self, track, xclip, value=None): '''Scroll right.''' self._move_nav(3) - def _move_nav(self, direction): - # type: (int) -> None - self.application().view.scroll_view( - Application.View.NavDirection(direction), '', False - ) + def show(self, what): + # type: (Text) -> None + self.application().view.show_view(what) + + def hide(self, what): + # type: (Text) -> None + self.application().view.hide_view(what) + + def focus(self, what): + # type: (Text) -> None + self.application().view.focus_view(what) + + # TODO: zoom, scroll + + def is_visible(self, what): + # type: (Text) -> bool + return self.application().view.is_view_visible(what) def move_to_first_device(self, track, xclip, value=None): # type: (None, None, None) -> None @@ -503,9 +524,10 @@ def move_to_last_device(self, track, xclip, value=None): '''Move to the last device on the track and scroll the view.''' self.focus_devices() if self.sel_track.devices: - self.song().view.select_device( - self.sel_track.devices[len(self.sel_track.devices) - 1] - ) + self.song().view.select_device(self.sel_track.devices[-1]) + # self.song().view.select_device( + # self.sel_track.devices[len(self.sel_track.devices) - 1] + # ) self.application().view.scroll_view( Application.View.NavDirection(3), 'Detail/DeviceChain', False ) @@ -531,78 +553,78 @@ def move_to_next_device(self, track, xclip, value=None): def focus_devices(self): '''Make sure devices are in focus and visible.''' - self.application().view.show_view('Detail') - self.application().view.show_view('Detail/DeviceChain') + self.show('Detail') + self.show('Detail/DeviceChain') def show_clip_view(self, track, xclip, value=None): # type: (None, None, None) -> None '''Show clip view.''' - self.application().view.show_view('Detail') - self.application().view.show_view('Detail/Clip') + self.show('Detail') + self.show('Detail/Clip') def show_track_view(self, track, xclip, value=None): # type: (None, None, None) -> None '''Show track view.''' - self.application().view.show_view('Detail') - self.application().view.show_view('Detail/DeviceChain') + self.show('Detail') + self.show('Detail/DeviceChain') def show_detail_view(self, track, xclip, value=None): # type: (None, None, None) -> None '''Toggle between showing/hiding detail view.''' - if self.application().view.is_view_visible('Detail'): - self.application().view.hide_view('Detail') + if self.is_visible('Detail'): + self.hide('Detail') else: - self.application().view.show_view('Detail') + self.show('Detail') def toggle_browser(self, track, xclip, value=None): # type: (None, None, None) -> None '''Hide/show browser and move focus to or from browser.''' - if self.application().view.is_view_visible('Browser'): - self.application().view.hide_view('Browser') - self.application().view.focus_view('') + if self.is_visible('Browser'): + self.hide('Browser') + self.focus('') else: - self.application().view.show_view('Browser') - self.application().view.focus_view('Browser') + self.show('Browser') + self.focus('Browser') def toggle_detail_view(self, track, xclip, value=None): # type: (None, None, None) -> None '''Toggle between clip and track view.''' - self.application().view.show_view('Detail') - if self.application().view.is_view_visible('Detail/Clip'): - self.application().view.show_view('Detail/DeviceChain') + self.show('Detail') + if self.is_visible('Detail/Clip'): + self.show('Detail/DeviceChain') else: - self.application().view.show_view('Detail/Clip') + self.show('Detail/Clip') def toggle_main_view(self, track, xclip, value=None): # type: (None, None, None) -> None '''Toggle between session and arrange view.''' - if self.application().view.is_view_visible('Session'): - self.application().view.show_view('Arranger') + if self.is_visible('Session'): + self.show('Arranger') else: - self.application().view.show_view('Session') + self.show('Session') def focus_browser(self, track, xclip, value=None): # type: (None, None, None) -> None '''Move the focus to the browser, show browser first if necessary. ''' - if not self.application().view.is_view_visible('Browser'): - self.application().view.show_view('Browser') - self.application().view.focus_view('Browser') + if not self.is_visible('Browser'): + self.show('Browser') + self.focus('Browser') def focus_detail(self, track, xclip, value=None): # type: (None, None, None) -> None '''Move the focus to the detail view, show detail first if necessary. ''' - if not self.application().view.is_view_visible('Detail'): - self.application().view.show_view('Detail') - self.application().view.focus_view('Detail') + if not self.is_visible('Detail'): + self.show('Detail') + self.focus('Detail') def focus_main(self, track, xclip, value=None): # type: (None, None, None) -> None '''Move the focus to the main focu.''' - self.application().view.focus_view('') + self.focus('') def adjust_horizontal_zoom(self, track, xclip, value): # type: (None, None, Text) -> None @@ -613,7 +635,7 @@ def adjust_horizontal_zoom(self, track, xclip, value): value = value.replace('ALL', '').strip() try: val = int(value) - except: + except Exception: return else: # TODO @@ -633,7 +655,7 @@ def adjust_vertical_zoom(self, track, xclip, value): value = value.replace('ALL', '').strip() # type: Text try: v = int(value) # type: int - except: + except Exception: return direct = (v > 0) for _ in range(abs(v) + 1): @@ -641,38 +663,36 @@ def adjust_vertical_zoom(self, track, xclip, value): # endregion - def adjust_tempo(self, track, xclip, args): + def adjust_tempo(self, track, xclip, arg, *rest): # type: (None, None, Text) -> None '''Adjust/set tempo or apply smooth synced ramp.''' self._tempo_ramp_active = False self._tempo_ramp_settings = [] - args = args.strip() - if args.startswith(('<', '>')): - factor = self.get_adjustment_factor(args, True) + if arg.startswith(('<', '>')): + factor = self.get_adjustment_factor(arg, True) self.song().tempo = max(20, min(999, (self.song().tempo + factor))) - elif args.startswith('*'): + elif arg.startswith('*'): try: - self.song().tempo = max(20, min(999, (self.song().tempo * float(args[1:])))) - except: + self.song().tempo = max(20, min(999, (self.song().tempo * float(arg[1:])))) + except Exception: pass - elif args.startswith('RAMP'): - arg_array = args.split() - if len(arg_array) == 3: + elif arg.startswith('RAMP'): + if len(rest) == 2: try: - ramp_factor = float("%.2f" % (int(arg_array[1]) * self.song().signature_numerator)) - if arg_array[2].startswith('*'): - target_tempo = max(20, min(999, (self.song().tempo * float(arg_array[2][1:])))) + if rest[1].startswith('*'): + target_tempo = max(20, min(999, (self.song().tempo * float(rest[1][1:])))) else: - target_tempo = float("%.2f" % float(arg_array[2])) - if target_tempo >= 20.0 and target_tempo <= 999.0: + target_tempo = float("%.2f" % float(rest[1])) + if 20.0 <= target_tempo <= 999.0: + ramp_factor = float("%.2f" % (int(rest[0]) * self.song().signature_numerator)) self._tempo_ramp_settings = [target_tempo, (target_tempo - self.song().tempo) / ramp_factor] self._tempo_ramp_active = True - except: + except Exception: pass else: try: - self.song().tempo = float(args) - except: + self.song().tempo = float(arg) + except Exception: pass def on_time_changed(self): @@ -699,55 +719,51 @@ def apply_tempo_ramp(self, arg=None): else: self.song().tempo += self._tempo_ramp_settings[1] - def adjust_groove(self, track, xclip, args): + def adjust_groove(self, track, xclip, arg): # type: (None, None, Text) -> None '''Adjust/set global groove.''' - args = args.strip() - if args.startswith(('<', '>')): - factor = self.get_adjustment_factor(args, True) + if arg.startswith(('<', '>')): + factor = self.get_adjustment_factor(arg, True) self.song().groove_amount = max(0.0, min(1.3125, self.song().groove_amount + factor * float(1.3125 / 131.0))) else: try: - self.song().groove_amount = int(args) * float(1.3125 / 131.0) - except: + self.song().groove_amount = int(arg) * float(1.3125 / 131.0) + except Exception: pass - def set_note_repeat(self, track, xclip, args): + def set_note_repeat(self, track, xclip, arg=None): # type: (None, None, Text) -> None '''Set/toggle note repeat.''' - args = args.strip() - if args == 'OFF': + if arg == 'OFF': self._parent._c_instance.note_repeat.enabled = False self._repeat_enabled = False - elif args in REPEAT_STATES: - self._parent._c_instance.note_repeat.repeat_rate = REPEAT_STATES[args] + elif arg in REPEAT_STATES: + self._parent._c_instance.note_repeat.repeat_rate = REPEAT_STATES[arg] self._parent._c_instance.note_repeat.enabled = True self._repeat_enabled = True else: self._repeat_enabled = not self._repeat_enabled self._parent._c_instance.note_repeat.enabled = self._repeat_enabled - def adjust_swing(self, track, xclip, args): + def adjust_swing(self, track, xclip, arg): # type: (None, None, Text) -> None '''Adjust swing amount for use with note repeat.''' - args = args.strip() - if args.startswith(('<', '>')): - factor = self.get_adjustment_factor(args, True) + if arg.startswith(('<', '>')): + factor = self.get_adjustment_factor(arg, True) self.song().swing_amount = max(0.0, min(1.0, (self.song().swing_amount + factor * 0.01))) else: try: - self.song().swing_amount = int(args) * 0.01 - except: + self.song().swing_amount = int(arg) * 0.01 + except Exception: pass - def adjust_global_quantize(self, track, xclip, args): + def adjust_global_quantize(self, track, xclip, arg=None): # type: (None, None, Text) -> None '''Adjust/set/toggle global quantization.''' - args = args.strip() - if args in GQ_STATES: - self.song().clip_trigger_quantization = GQ_STATES[args] - elif args in ('<', '>'): - factor = self.get_adjustment_factor(args) + if arg in GQ_STATES: + self.song().clip_trigger_quantization = GQ_STATES[arg] + elif arg.startswith(('<', '>')): + factor = self.get_adjustment_factor(arg) new_gq = self.song().clip_trigger_quantization + factor if 0 <= new_gq < 14: self.song().clip_trigger_quantization = new_gq @@ -757,14 +773,13 @@ def adjust_global_quantize(self, track, xclip, args): else: self.song().clip_trigger_quantization = self._last_gqntz - def adjust_record_quantize(self, track, xclip, args): + def adjust_record_quantize(self, track, xclip, arg): # type: (None, None, Text) -> None '''Adjust/set/toggle record quantization.''' - args = args.strip() - if args in RQ_STATES: - self.song().midi_recording_quantization = RQ_STATES[args] - elif args in ('<', '>'): - factor = self.get_adjustment_factor(args) + if arg in RQ_STATES: + self.song().midi_recording_quantization = RQ_STATES[arg] + elif arg.startswith(('<', '>')): + factor = self.get_adjustment_factor(arg) new_rq = self.song().midi_recording_quantization + factor if 0 <= new_rq < 9: self.song().midi_recording_quantization = new_rq @@ -774,46 +789,49 @@ def adjust_record_quantize(self, track, xclip, args): else: self.song().midi_recording_quantization = self._last_rqntz - def adjust_time_signature(self, track, xclip, args): + def adjust_time_signature(self, track, xclip, arg): # type: (None, None, Text) -> None '''Adjust global time signature.''' - if '/' in args: - try: - num, denom = args.split('/') - self.song().signature_numerator = int(num) - self.song().signature_denominator = int(denom) - except: - pass + try: + num, denom = map(int, arg.split('/')) + self.song().signature_numerator = num + self.song().signature_denominator = denom + except Exception as e: + log.error("Failed to set time signature '%s': %r", arg, e) - def set_jump_all(self, track, xclip, args): + def set_jump_all(self, track, xclip, arg): # type: (None, None, Text) -> None '''Jump arrange position forward/backward.''' try: - self.song().jump_by(float(args)) - except: + self.song().jump_by(float(arg)) + except Exception: pass - def set_unarm_all(self, track, xclip, args): + # TODO: to track actions + def set_unarm_all(self, track, xclip, *args): # type: (None, None, None) -> None '''Unarm all armable track.''' for t in self.song().tracks: if t.can_be_armed and t.arm: t.arm = 0 - def set_unmute_all(self, track, xclip, args): + # TODO: to track actions + def set_unmute_all(self, track, xclip, *args): # type: (None, None, None) -> None '''Unmute all track.''' for t in chain(self.song().tracks, self.song().return_tracks): if t.mute: t.mute = 0 - def set_unsolo_all(self, track, xclip, args): + # TODO: to track actions + def set_unsolo_all(self, track, xclip, *args): # type: (None, None, None) -> None '''Unsolo all track.''' for t in chain(self.song().tracks, self.song().return_tracks): if t.solo: t.solo = 0 + # TODO: to track actions def set_fold_all(self, track, xclip, value): # type: (None, None, None) -> None '''Toggle or turn/on fold for all track.''' @@ -824,66 +842,64 @@ def set_fold_all(self, track, xclip, value): state_to_set = not t.fold_state switch(t, 'fold_state', value, state_to_set) - def set_locator(self, track, xclip, args): + def set_locator(self, track, xclip, *args): # type: (None, None, None) -> None '''Set/delete a locator at the current playback position.''' self.song().set_or_delete_cue() - def do_locator_loop_action(self, track, xclip, args): + def do_locator_loop_action(self, track, xclip, arg): # type: (None, None, Text) -> None '''Same as do_locator_action with name argument, but also sets arrangement loop start to pos of locator. ''' - self.do_locator_action(track, xclip, args, True) + self.do_locator_action(track, xclip, arg, True) - def do_locator_action(self, track, xclip, args, move_loop_too=False): + def do_locator_action(self, track, xclip, arg, move_loop_too=False): # type: (None, None, Text, bool) -> None '''Jump between locators or to a particular locator. Can also move loop start to pos of locator if specified. ''' - args = args.strip() - if args == '>' and self.song().can_jump_to_next_cue: + if arg == '>' and self.song().can_jump_to_next_cue: self.song().jump_to_next_cue() - elif args == '<' and self.song().can_jump_to_prev_cue: + elif arg == '<' and self.song().can_jump_to_prev_cue: self.song().jump_to_prev_cue() else: try: for cp in self.song().cue_points: - if cp.name.upper() == args: + if cp.name.upper() == arg: cp.jump() if move_loop_too: self.song().loop_start = cp.time break - except: + except Exception: pass - def do_loop_action(self, track, xclip, args): + def do_loop_action(self, track, xclip, arg=None): # type: (None, None, Text) -> None '''Handle arrange loop action.''' - args = args.strip() - if not args or args.upper() in KEYWORDS: - self.set_loop_on_off(args) + if not arg or arg.upper() in KEYWORDS: + self.set_loop_on_off(arg) else: new_start = self.song().loop_start new_length = self.song().loop_length - if args.startswith(('<', '>')): - self.move_loop_by_factor(args) + if arg.startswith(('<', '>')): + self.move_loop_by_factor(arg) return - elif args == 'RESET': + elif arg == 'RESET': new_start = 0 - elif args.startswith('*'): + elif arg.startswith('*'): try: - new_length = self.song().loop_length * float(args[1:]) - except: + new_length = self.song().loop_length * float(arg[1:]) + except Exception: pass else: try: # TODO: int? - new_length = float(args) * ( + new_length = float(arg) * ( (4.0 / self.song().signature_denominator) * self.song().signature_numerator ) - except: + except Exception: pass self.set_new_loop_position(new_start, new_length) @@ -892,15 +908,15 @@ def set_loop_on_off(self, value=None): '''Toggles or turns on/off arrange loop.''' switch(self.song(), 'loop', value) - def move_loop_by_factor(self, args): + def move_loop_by_factor(self, arg): # type: (Text) -> None '''Move arrangement loop by its length or by a specified factor. ''' factor = self.song().loop_length - if args == '<': + if arg == '<': factor = -(factor) - elif len(args) > 1: - factor = self.get_adjustment_factor(args, True) + elif len(ars) > 1: + factor = self.get_adjustment_factor(arg, True) new_start = self.song().loop_start + factor if new_start < 0.0: new_start = 0.0 diff --git a/src/clyphx/actions/push.py b/src/clyphx/actions/push.py index eb00993..22c37d6 100644 --- a/src/clyphx/actions/push.py +++ b/src/clyphx/actions/push.py @@ -115,7 +115,7 @@ def handle_session_offset(self, session, last_pos, args, parser): return session.set_offsets(new_track, new_scene) return return_val - except: + except Exception: pass def get_session_offsets(self, session): @@ -299,7 +299,7 @@ def _recall_scale_settings(self, arg_array): int(arg_array[1]) layout.is_fixed = arg_array[3] == 'TRUE' layout.is_in_key = arg_array[4] == 'TRUE' - except: + except Exception: pass def _update_scale_display_and_buttons(self): diff --git a/src/clyphx/actions/pxt_live.py b/src/clyphx/actions/pxt_live.py index 2642c74..96b37de 100644 --- a/src/clyphx/actions/pxt_live.py +++ b/src/clyphx/actions/pxt_live.py @@ -26,7 +26,7 @@ from _NKFW.Scales import SCALE_TYPES from _NKFW.ScalesComponent import EDITABLE_SCALE HAS_PXT = True -except: +except Exception: HAS_PXT = False UNWRITABLE_INDEXES = (17, 35, 53) @@ -127,7 +127,7 @@ def _handle_poly_seq_action(self, args, xclip, ident): if 0 <= lane_num < comp._num_note_lanes: notes = str(comp._note_lane_components[lane_num]._note) args = args.replace(lane_spec, '') - except: + except Exception: pass if notes: start = comp._position_component._start_position diff --git a/src/clyphx/actions/scene.py b/src/clyphx/actions/scene.py index 142bc97..0c20805 100644 --- a/src/clyphx/actions/scene.py +++ b/src/clyphx/actions/scene.py @@ -2,6 +2,7 @@ from builtins import object from typing import TYPE_CHECKING import logging +from ..core.live import get_random_int if TYPE_CHECKING: from typing import Optional, Text, Tuple @@ -82,11 +83,11 @@ def set_scene(self, track, xclip, args): if len(rnd_range_data) == 2: try: new_min = int(rnd_range_data[0]) - 1 - except: + except Exception: new_min = 0 try: new_max = int(rnd_range_data[1]) - except: + except Exception: new_max = num_scenes if 0 < new_min and new_max < num_scenes + 1 and new_min < new_max - 1: rnd_range = [new_min, new_max] @@ -105,7 +106,8 @@ def set_scene(self, track, xclip, args): scene = -(abs(scene) - len(self.song().scenes)) self._last_scene_index = scene for t in self.song().tracks: - if not (t.is_foldable or (t.clip_slots[scene].has_clip and t.clip_slots[scene].clip == xclip)): + if not (t.is_foldable or (t.clip_slots[scene].has_clip + and t.clip_slots[scene].clip == xclip)): t.clip_slots[scene].fire() def get_scene_to_operate_on(self, xclip, args): @@ -119,15 +121,19 @@ def get_scene_to_operate_on(self, xclip, args): scene_name = args[args.index('"')+1:] if '"' in scene_name: scene_name = scene_name[0:scene_name.index('"')] - for i in range(len(self.song().scenes)): - if scene_name == self.song().scenes[i].name.upper(): + for i, sc in enumerate(self.song().scenes): + if scene_name == sc.name.upper(): scene = i break + # for i in range(len(self.song().scenes)): + # if scene_name == self.song().scenes[i].name.upper(): + # scene = i + # break elif args and args != 'SEL': try: if 0 <= int(args) < len(self.song().scenes) + 1: scene = int(args) - 1 - except: + except Exception: pass return scene diff --git a/src/clyphx/actions/snap.py b/src/clyphx/actions/snap.py index 32a80a2..ce3786e 100644 --- a/src/clyphx/actions/snap.py +++ b/src/clyphx/actions/snap.py @@ -35,28 +35,28 @@ # SNAP DATA ARRAY # Positions of the main categories -MIX_STD_SETTINGS_POS = 0 -MIX_EXT_SETTINGS_POS = 1 -PLAY_SETTINGS_POS = 2 -DEVICE_SETTINGS_POS = 3 +MIX_STD_SETTINGS = 0 +MIX_EXT_SETTINGS = 1 +PLAY_SETTINGS = 2 +DEVICE_SETTINGS = 3 # ASSOCIATED ARRAY # Positions of standard mix settings -MIX_VOL_POS = 0 -MIX_PAN_POS = 1 -MIX_SEND_START_POS = 2 +MIX_VOL = 0 +MIX_PAN = 1 +MIX_SEND_START = 2 # Positions of extended mix settings -MIX_MUTE_POS = 0 -MIX_SOLO_POS = 1 -MIX_CF_POS = 2 +MIX_MUTE = 0 +MIX_SOLO = 1 +MIX_CF = 2 # Positions of chain mix settings -CHAIN_VOL_POS = 0 -CHAIN_PAN_POS = 1 -CHAIN_MUTE_POS = 2 -CHAIN_SEND_START_POS = 3 +CHAIN_VOL = 0 +CHAIN_PAN = 1 +CHAIN_MUTE = 2 +CHAIN_SEND_START = 3 class XSnapActions(XComponent): @@ -120,7 +120,7 @@ def store_track_snapshot(self, track_list, xclip, ident, args): if not args or 'MIX' in args: param_count += self._store_mix_settings(track, args) if 'PLAY' in args and track in self.song().tracks: - self._current_track_data[PLAY_SETTINGS_POS] = track.playing_slot_index + self._current_track_data[PLAY_SETTINGS] = track.playing_slot_index param_count += 1 if (not args or 'DEV' in args) and track.devices: param_count += self._store_device_settings(track, args) @@ -147,9 +147,9 @@ def _store_mix_settings(self, track, args): if not 'MIX-' in args: mix_vals.extend([s.value for s in track.mixer_device.sends]) param_count += len(mix_vals) - self._current_track_data[MIX_STD_SETTINGS_POS] = mix_vals + self._current_track_data[MIX_STD_SETTINGS] = mix_vals if ('MIX+' in args or 'MIX-' in args) and track != self.song().master_track: - self._current_track_data[MIX_EXT_SETTINGS_POS] = [ + self._current_track_data[MIX_EXT_SETTINGS] = [ int(track.mute), int(track.solo), track.mixer_device.crossfade_assign ] param_count += 3 @@ -183,7 +183,7 @@ def _store_device_settings(self, track, args): current_device, track_devices[current_device.name], 0 ) if track_devices: - self._current_track_data[DEVICE_SETTINGS_POS] = track_devices + self._current_track_data[DEVICE_SETTINGS] = track_devices return param_count @@ -228,7 +228,7 @@ def recall_track_snapshot(self, name, xclip, disable_smooth=False): is_synced = False if disable_smooth else self._init_smoothing(xclip) for track, param_data in snap_data.items(): - pos = param_data[PLAY_SETTINGS_POS] + pos = param_data[PLAY_SETTINGS] if track in self.current_tracks: track = self.current_tracks[track] self._recall_mix_settings(track, param_data) @@ -254,34 +254,34 @@ def recall_track_snapshot(self, name, xclip, disable_smooth=False): def _recall_mix_settings(self, track, param_data): # type: (Track, Mapping[int, Any]) -> None '''Recalls mixer related settings.''' - std = param_data[MIX_STD_SETTINGS_POS] + std = param_data[MIX_STD_SETTINGS] if std: - pan_value = std[MIX_PAN_POS] - if (track.mixer_device.volume.is_enabled and std[MIX_VOL_POS] != -1): + pan_value = std[MIX_PAN] + if (track.mixer_device.volume.is_enabled and std[MIX_VOL] != -1): self._get_parameter_data_to_smooth( - track.mixer_device.volume, std[MIX_VOL_POS]) + track.mixer_device.volume, std[MIX_VOL]) if track.mixer_device.panning.is_enabled and not isinstance(pan_value, int): self._get_parameter_data_to_smooth( - track.mixer_device.panning, std[MIX_PAN_POS]) + track.mixer_device.panning, std[MIX_PAN]) if track is not self.song().master_track: sends = track.mixer_device.sends - for i in range(len(std) - MIX_SEND_START_POS): + for i in range(len(std) - MIX_SEND_START): if (i <= len(sends) - 1 and sends[i].is_enabled): self._get_parameter_data_to_smooth( - sends[i], std[MIX_SEND_START_POS + i]) + sends[i], std[MIX_SEND_START + i]) if param_data[1] and track is not self.song().master_track: - ext = param_data[MIX_EXT_SETTINGS_POS] - track.mute = ext[MIX_MUTE_POS] - track.solo = ext[MIX_SOLO_POS] - track.mixer_device.crossfade_assign = ext[MIX_CF_POS] + ext = param_data[MIX_EXT_SETTINGS] + track.mute = ext[MIX_MUTE] + track.solo = ext[MIX_SOLO] + track.mixer_device.crossfade_assign = ext[MIX_CF] def _recall_device_settings(self, track, param_data): # type: (Track, Mapping[int, Any]) -> None '''Recalls device related settings.''' - settings = param_data[DEVICE_SETTINGS_POS] + settings = param_data[DEVICE_SETTINGS] for device in track.devices: if device.name in settings: self._recall_device_snap(device, settings[device.name]['params']) @@ -329,26 +329,26 @@ def _recall_nested_device_snap(self, rack, stored_params): if chain.mixer_device.volume.is_enabled: self._get_parameter_data_to_smooth( chain.mixer_device.volume, - stored_chain['mixer'][CHAIN_VOL_POS], + stored_chain['mixer'][CHAIN_VOL], ) if chain.mixer_device.panning.is_enabled: self._get_parameter_data_to_smooth( chain.mixer_device.panning, - stored_chain['mixer'][CHAIN_PAN_POS], + stored_chain['mixer'][CHAIN_PAN], ) if chain.mixer_device.chain_activator.is_enabled: self._get_parameter_data_to_smooth( chain.mixer_device.chain_activator, - stored_chain['mixer'][CHAIN_MUTE_POS], + stored_chain['mixer'][CHAIN_MUTE], ) sends = chain.mixer_device.sends if sends: num_sends = len(sends) - for i in range(len(stored_chain['mixer']) - CHAIN_SEND_START_POS): + for i in range(len(stored_chain['mixer']) - CHAIN_SEND_START): if i < num_sends and sends[i].is_enabled: self._get_parameter_data_to_smooth( sends[i], - stored_chain['mixer'][CHAIN_SEND_START_POS + i], + stored_chain['mixer'][CHAIN_SEND_START + i], ) def _init_smoothing(self, xclip): @@ -372,7 +372,7 @@ def _init_smoothing(self, xclip): is_synced = 'S' in speed try: new_speed = int(speed.replace('S', '')) - except: + except Exception: new_speed = 8 else: if '[' and ']' in track_name: @@ -380,7 +380,7 @@ def _init_smoothing(self, xclip): is_synced = 'S' in speed try: new_speed = int(speed.replace('S', '')) - except: + except Exception: new_speed = 8 if is_synced: new_speed *= self.song().signature_numerator @@ -512,13 +512,13 @@ def _get_snap_device_range(args, track): try: start, end = map(int, dev_args.split('-')) start -= 1 - except: + except Exception: pass else: try: start = int(dev_args) - 1 end = start + 1 - except: + except Exception: pass if end > len(track.devices) or start < 0 or end < start: diff --git a/src/clyphx/actions/track.py b/src/clyphx/actions/track.py index 162a7e0..05f6f5a 100644 --- a/src/clyphx/actions/track.py +++ b/src/clyphx/actions/track.py @@ -71,7 +71,7 @@ def delete_device(self, track, xclip, args): index = int(args) - 1 if index < len(track.devices): track.delete_device(index) - except: + except Exception: pass def create_clip(self, track, xclip, args): @@ -91,12 +91,12 @@ def create_clip(self, track, xclip, args): if specified_slot != 'SEL': try: slot = int(specified_slot) - 1 - except: + except Exception: pass if len(arg_array) > 1: try: length = float(arg_array[1].strip()) * bar - except: + except Exception: pass if 0 <= slot < len(self.song().scenes): if not track.clip_slots[slot].has_clip: @@ -178,7 +178,7 @@ def set_selection(self, track, xclip, args): if args: try: self.song().view.selected_scene = list(self.song().scenes)[int(args) - 1] - except: + except Exception: pass elif track.playing_slot_index >= 0: self.song().view.selected_scene = list(self.song().scenes)[track.playing_slot_index] @@ -189,7 +189,7 @@ def set_jump(self, track, xclip, args): if track in self.song().tracks: try: track.jump_in_running_session_clip(float(args)) - except: + except Exception: pass def set_stop(self, track, xclip, value=None): @@ -279,11 +279,11 @@ def _get_slot_index_to_play(self, track, xclip, args, allow_empty_slots=False): if len(rnd_range_data) == 2: try: new_min = int(rnd_range_data[0]) - 1 - except: + except Exception: new_min = 0 try: new_max = int(rnd_range_data[1]) - except: + except Exception: new_max = num_scenes if 0 <= new_min and new_max < num_scenes + 1 and new_min < new_max - 1: rnd_range = [new_min, new_max] @@ -323,7 +323,7 @@ def _get_slot_index_to_play(self, track, xclip, args, allow_empty_slots=False): try: if 0 <= int(args) < len(self.song().scenes) + 1: slot_to_play = int(args) - 1 - except: + except Exception: pass if ((not track.clip_slots[slot_to_play].has_clip and allow_empty_slots) @@ -373,7 +373,7 @@ def get_send_parameter(self, track, send_string): if track != self.song().master_track: try: param = track.mixer_device.sends[ord(send_string) - 65] - except: + except Exception: pass return param diff --git a/src/clyphx/clyphx.py b/src/clyphx/clyphx.py index 8fb1fd3..5bb4925 100644 --- a/src/clyphx/clyphx.py +++ b/src/clyphx/clyphx.py @@ -24,7 +24,7 @@ from _Framework.ControlSurface import OptimizedControlSurface from .core.legacy import _DispatchCommand, _SingleDispatch from .core.utils import repr_tracklist, set_user_profile -from .core.live import Live, Track, DeviceIO, Clip, get_random_int +from .core.live import Live, Track, Clip, get_random_int from .core.parse import IdSpecParser, ObjParser from .core.xcomponent import XComponent from .consts import LIVE_VERSION, SCRIPT_INFO @@ -261,8 +261,6 @@ def run_statement(self, track, xtrigger): elif isinstance(xtrigger, Clip): # X-Clips can have on and off action lists - # TODO: if xtrigger.is_triggered? - log.info('Clip is triggered: %s, is_playing: %s', xtrigger.is_triggered, xtrigger.is_playing) if not xtrigger.is_playing: if not spec.off: return @@ -332,9 +330,9 @@ def _handle_action_list_trigger(self, track, xtrigger): raw_action_list = raw_action_list.replace('(PSEQ)', '').strip() # check if the trigger is a LSEQ (accessible only to X-Clips) - elif (isinstance(xtrigger, Clip) and - raw_action_list[0] == '(' and - '(LSEQ)' in raw_action_list): + elif (isinstance(xtrigger, Clip) + and raw_action_list[0] == '(' + and '(LSEQ)' in raw_action_list): is_loop_seq = True raw_action_list = raw_action_list.replace('(LSEQ)', '').strip() @@ -365,7 +363,7 @@ def _handle_action_list_trigger(self, track, xtrigger): self.handle_dispatch_command(command) def _format_action_list(self, track, alist): - # (Text) -> List[Text] + # (Text) -> List[Dict[Text, Any]] alist = [self.format_action_name(track, a) for a in alist] return list(filter(None, alist)) @@ -442,47 +440,48 @@ def get_track_to_operate_on(self, origin_name): tracks = self.song().tracks + self.song().return_tracks + (self.song().master_track,) # type: Tuple[Track] sel_track_index = tracks.index(self.song().view.selected_track) if origin_name.index('/') > 0: - track_spec = origin_name.split('/')[0].strip() - if '"' in track_spec: - track_spec = self.get_track_index_by_name(track_spec, tracks) - if 'SEL' in track_spec: - track_spec = track_spec.replace('SEL', str(sel_track_index + 1), 1) - if 'MST' in track_spec: - track_spec = track_spec.replace('MST', str(len(tracks)), 1) - if track_spec == 'ALL': + spec = origin_name.split('/')[0].strip() + if '"' in spec: + spec = self.get_track_index_by_name(spec, tracks) + if 'SEL' in spec: + spec = spec.replace('SEL', str(sel_track_index + 1), 1) + if 'MST' in spec: + spec = spec.replace('MST', str(len(tracks)), 1) + if spec == 'ALL': result_tracks = tracks else: - track_range_spec = track_spec.split('-') - if len(track_range_spec) <= 2: + range_spec = spec.split('-') + if len(range_spec) <= 2: track_range = [] try: - for spec in track_range_spec: + for spec in range_spec: track_index = -1 if spec.startswith(('<', '>')): try: # FIXME: track_index = (XComponent.get_adjustment_factor(spec) + sel_track_index) - except: + except Exception: pass else: try: track_index = int(spec) - 1 - except: + except Exception: track_index = ((ord(spec) - 65) + len(self.song().tracks)) if 0 <= track_index < len(tracks): track_range.append(track_index) - except: + except Exception as e: + log.error("Failed to parse tracks '%s': %r", spec, e) track_range = [] if track_range: - if len(track_range) == 2: - if track_range[0] < track_range[1]: - for i in range(track_range[0], track_range[1] + 1): - result_tracks.append(tracks[i]) - else: + try: + indices = range(track_range[0], track_range[1] + 1) + except IndexError: result_tracks = [tracks[track_range[0]]] + else: + result_tracks = [tracks[i] for i in indices] result_name = origin_name[origin_name.index('/') + 1:].strip() log.debug('get_track_to_operate_on -> result_tracks=%s, result_name=%s', repr_tracklist(result_tracks), result_name) diff --git a/src/clyphx/consts.py b/src/clyphx/consts.py index 65bfd84..8a2f699 100644 --- a/src/clyphx/consts.py +++ b/src/clyphx/consts.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, unicode_literals from builtins import map, dict from typing import TYPE_CHECKING +# from fraction import Fraction import logging from _Generic.Devices import DEVICE_DICT, DEVICE_BOB_DICT @@ -28,7 +29,7 @@ get_application) if TYPE_CHECKING: - from typing import Any, Optional, Sequence, Text, Dict, Mapping, TypeVar + from typing import Any, Optional, Sequence, Text, Mapping, TypeVar from .core.live import Application T = TypeVar('T') @@ -48,6 +49,7 @@ KEYWORDS = dict(ON=1, OFF=0) # type: Mapping[Optional[Text], bool] + def switch(obj, attr, value, fallback=unset): # type: (T, Text, Text, Any) -> None '''Turns object attribute on/off or toggles (if there is no fallback). @@ -58,6 +60,7 @@ def switch(obj, attr, value, fallback=unset): v = (not getattr(obj, attr)) if fallback is unset else fallback setattr(obj, attr, v) + ONOFF = dict(ON=True, OFF=False) # type: Mapping[Text, bool] NOTE_NAMES = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B') @@ -73,9 +76,12 @@ def switch(obj, attr, value, fallback=unset): ) MIDI_STATUS = dict( - NOTE = 144, - CC = 176, - PC = 192, + NOTE = 144, + CC = 176, + PC = 192, # program change + CA = 208, # channel aftertouch + PB = 224, # pitch bend change + SYSEX = 240, ) # type: Mapping[Text, int] WARP_MODES = dict(( @@ -183,6 +189,19 @@ def switch(obj, attr, value, fallback=unset): ('1/32', 0.125), ('1/32T', 0.0833333333333), )) # type: Mapping[Text, float] + +# REPEAT_STATES = dict(( +# ('OFF', Fraction(1, 1)), +# ('1/4', Fraction(1, 1)), +# ('1/4T', Fraction(2, 3)), +# ('1/8', Fraction(1, 2)), +# ('1/8T', Fraction(1, 3)), +# ('1/16', Fraction(1, 4)), +# ('1/16T', Fraction(1, 6)), +# ('1/32', Fraction(1, 8)), +# ('1/32T', Fraction(1, 12)), +# )) # type: Mapping[Text, Fraction] + # endregion # region DEVICES @@ -255,14 +274,19 @@ def switch(obj, attr, value, fallback=unset): MIDI_DEVS = dict((dev.upper(), dev) for dev in _MIDI_DEVS) +# TODO: Wavetable, Ping Pong Delay +# TODO: update previous dicts #: Translation table between API names and friendly names. DEV_NAME_TRANSLATION = dict( UltraAnalog = 'Analog', MidiArpeggiator = 'Arpeggiator', AudioEffectGroupDevice = 'Audio Effect Rack', + AutoFilter = 'Auto Filter', + AutoPan = 'Auto Pan', BeatRepeat = 'Beat Repeat', MidiChord = 'Chord', Compressor2 = 'Compressor', + DrumBuss = 'Drum Buss', DrumGroupDevice = 'Drum Rack', Tube = 'Dynamic Tube', LoungeLizard = 'Electric', @@ -279,9 +303,10 @@ def switch(obj, attr, value, fallback=unset): MidiNoteLength = 'Note Length', MidiPitcher = 'Pitch', MidiRandom = 'Random', + Resonator = 'Resonators', MultiSampler = 'Sampler', MidiScale = 'Scale', - CrossDelay = 'Simple Delay', + CrossDelay = 'Simple Delay', # FIXME: missing OriginalSimpler = 'Simpler', SpectrumAnalyzer = 'Spectrum', StringStudio = 'Tension', diff --git a/src/clyphx/core/action.py b/src/clyphx/core/action.py new file mode 100644 index 0000000..fb9c0b5 --- /dev/null +++ b/src/clyphx/core/action.py @@ -0,0 +1,67 @@ +class Action(object): + __slots__ = 'action', 'args' + + def __init__(self, action=None, *args): + self.action = action + self.args = args + + def get_action(self): + raise NotImplementedError + + def run(self): + raise NotImplementedError + + +class GlobalAction(Action): + __slots__ = () + + +class BrowserAction(Action): + __slots__ = () + + +class TrackAction(Action): + __slots__ = 'tracks', + + def __init__(self, action=None, *args, tracks): + self.tracks = tracks + super().__init__(args) + + def get_action(self): + pass + + def run(self): + pass + + +class DeviceAction(TrackAction): + __slots__ = 'devices', + + def __init__(self, action, *args, tracks, devices): + self.devices = devices + super().__init__(args, tracks) + + +class ClipAction(TrackAction): + __slots__ = 'clips', + + def __init__(self, action, *args, tracks, clips): + self.clips = clips + super().__init__(args, tracks) + + +class NotesAction(ClipAction): + __slots__ = 'notes', + + def __init__(self, action, *args, tracks, clips, notes): + self.notes = notes + super().__init__(args, tracks, clips) + + def iter_target(self): + for clip in super().iter_target(): + if clip.is_audio: + continue + + def get_action(self): + # func = NOTES_ACTIONS[action] + pass diff --git a/src/clyphx/core/exceptions.py b/src/clyphx/core/exceptions.py index d06d299..1da5346 100644 --- a/src/clyphx/core/exceptions.py +++ b/src/clyphx/core/exceptions.py @@ -1,18 +1,23 @@ class ClyphXception(Exception): def __init__(self, msg=None, *args, **kwargs): - suoer().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + class ParsingError(ClyphXception, ValueError): pass + class InvalidSpec(ClyphXception, ValueError): pass + class InvalidParam(ClyphXception, ValueError): pass + class InvalidAction(ClyphXception, TypeError): pass + class ActionUnavailable(ClyphXception, TypeError): pass diff --git a/src/clyphx/core/legacy.py b/src/clyphx/core/legacy.py index ffc46df..a56ede5 100644 --- a/src/clyphx/core/legacy.py +++ b/src/clyphx/core/legacy.py @@ -12,7 +12,7 @@ from .utils import repr_tracklist if TYPE_CHECKING: - from typing import Union, Text, Sequence, Iterator + from typing import Text, Sequence, Iterator from .live import Track, Clip log = logging.getLogger(__name__) @@ -57,6 +57,7 @@ def __init__(self, track, xclip, ident, action_name, args): if len(track) == 1: track = track[0] else: - raise TypeError('SingleDispatch should receive an only track: %s', track) + msg = 'SingleDispatch should receive an only track: {}' + raise TypeError(msg.format(track)) super().__init__([track], xclip, ident, action_name, args) self.track = track diff --git a/src/clyphx/core/live.py b/src/clyphx/core/live.py index dbf15ed..1ddd624 100644 --- a/src/clyphx/core/live.py +++ b/src/clyphx/core/live.py @@ -10,25 +10,25 @@ Browser, Chain, Clip, - CompressorDevice, + # CompressorDevice, Conversions, Device, - DeviceIO, + # DeviceIO, DeviceParameter, DrumChain, DrumPad, Eq8Device, - LomObject, + # LomObject, MidiMap, MixerDevice, PluginDevice, RackDevice, - Sample, + # Sample, Scene, - SimplerDevice, + # SimplerDevice, Song, Track, - WavetableDevice, + # WavetableDevice, ) # import classes and functions @@ -42,20 +42,17 @@ ) from Chain import Chain from Clip import Clip, AutomationEnvelope, GridQuantization, WarpMode -from CompressorDevice import CompressorDevice +# from CompressorDevice import CompressorDevice from Device import Device, DeviceType from DeviceParameter import DeviceParameter, ParameterState, AutomationState from DrumPad import DrumPad -from LomObject import LomObject -from MidiMap import ( - forward_midi_cc, - forward_midi_note, -) +# from LomObject import LomObject +from MidiMap import forward_midi_cc, forward_midi_note from MixerDevice import MixerDevice from RackDevice import RackDevice -from Sample import Sample, TransientLoopMode, SlicingStyle, SlicingBeatDivision +# from Sample import Sample, TransientLoopMode, SlicingStyle, SlicingBeatDivision from Scene import Scene -from SimplerDevice import SimplerDevice, SlicingPlaybackMode, PlaybackMode +# from SimplerDevice import SimplerDevice, SlicingPlaybackMode, PlaybackMode from Song import Song, RecordingQuantization from Track import ( Track, DeviceContainer, RoutingTypeCategory, diff --git a/src/clyphx/core/models.py b/src/clyphx/core/models.py index dc2577f..096571d 100644 --- a/src/clyphx/core/models.py +++ b/src/clyphx/core/models.py @@ -124,6 +124,11 @@ def pitch_range(string): ('mute', bool)]) +Location = NamedTuple('Location', [('bar', int), + ('beat', int), + ('sixteenth', int)]) + + class UserControl(object): ''' X-Controls defined in user settings. diff --git a/src/clyphx/core/parse.py b/src/clyphx/core/parse.py index 5e48e31..7f8b7a6 100644 --- a/src/clyphx/core/parse.py +++ b/src/clyphx/core/parse.py @@ -6,6 +6,7 @@ from __future__ import absolute_import, unicode_literals from builtins import map, object, dict +from operator import neg, pos, add from typing import TYPE_CHECKING import re @@ -23,8 +24,8 @@ ) NONTERMINALS = dict( - LISTS = r'(?P[\w<>"]\S.*?)', - TOKENS = r'(?=[^|;]\s*?)(\S.*?)\s*?(?=$|;)', + LISTS = r'(?P[\w<>"]\S.*?)', + ACTIONS = r'(?=[^|;]\s*?)(\S.*?)\s*?(?=$|;)', ) SYMBOLS = TERMINALS.copy() @@ -46,7 +47,7 @@ class IdSpecParser(object): (?:\s*?[:]\s*? (?P\S[^:]*?))?\s*?$ ''', re.X) - tokens = re.compile(NONTERMINALS['TOKENS']) + actions = re.compile(NONTERMINALS['ACTIONS']) def _parse(self, string): # type: (Text) -> IdSpec @@ -56,7 +57,7 @@ def _parse(self, string): # split lists lists = self.lists.match(spec.pop('lists')).groupdict().items() - spec.update({k: self.tokens.findall(v) if v else v for k, v in lists}) + spec.update({k: self.actions.findall(v) if v else v for k, v in lists}) if spec['override']: spec.update(id=spec['override'], override=True) @@ -73,19 +74,60 @@ def __call__(self, string): raise ParsingError(string) +TERMINALS = dict() + +NONTERMINALS = dict( + TRACKS = '^.*?/', + TOKENS = '', +) + + +class ActionParser(object): + + tracks = re.compile('', re.I) + tokens = re.compile('', re.I) + + def _parse(self, string): + pass + + def __call__(self, string): + try: + return self._parse(string) + except Exception: + raise ParsingError(string) + + TERMINALS = dict( - POS = r'(?P\d+?)', + POS = r'(?P[1-9]\d?)', # 1-99 SEL = r'(?PSEL)', NAME = r'\"(?P[A-Z0-9\-<>\s]+?)?\"', + ADD = r'(?P[><])(?P\d?(?P\.\d+?)?)', +) + +TARGETS = dict( + CLIP = r'(?PCLIP(?P\(?{POS}|{SEL}|{NAME}\)?)?)', + DEV = r'(?PDEV(?P\(?...\)?)?)', + DR = r'(?PDR(?P\(?...\)?)?)', + CH = r'(?PCH(?P\(?...\)?)?)', + PAD = r'(?PPAD(?P\(?...\)?)?)', + NOTES = r'(?PNOTES(?P\(?...\)?)?)', ) class ObjParser(object): - clip = re.compile(r'CLIP({POS}|{SEL}|{NAME})?'.format(**TERMINALS), re.I) + clip = re.compile(r'CLIP(?P{POS}|{SEL}|{NAME})?'.format(**TERMINALS), re.I) def _parse(self, kind, string): match = getattr(self, kind).match(string) + + # # if only fractional part, it's a float in the format: >.012 + # if match.get('add'): + # func = float if match.pop('addfract') else int + # value = func(match.pop('addval') or (0 if func is float else 1)) + # sign = {'>': pos, '<': neg}[match.pop('add')] + # match['op'] = partial(add, sign(value)) + return {k: v for k, v in match.groupdict().items() if v} def __call__(self, kind, string): @@ -96,6 +138,6 @@ def __call__(self, kind, string): raise ParsingError(string) -__all__ = ['IdSpecParser', 'ObjParser'] +__all__ = ['IdSpecParser', 'ActionParser', 'ObjParser'] del (TERMINALS, NONTERMINALS, SYMBOLS) diff --git a/src/clyphx/core/utils.py b/src/clyphx/core/utils.py index 5637451..b26c7fc 100644 --- a/src/clyphx/core/utils.py +++ b/src/clyphx/core/utils.py @@ -18,7 +18,7 @@ def repr_slots(self): return str('{}({})'.format( type(self).__name__, ', '.join('{}={}'.format(k, getattr(self, k)) - for k in self.__slots__), + for k in self.__slots__), )) @@ -32,7 +32,7 @@ def repr_tracklist(tracks): else: try: return '[{}]'.format(', '.join(t.name for t in tracks)) - except: + except Exception: return '[ERROR {}]'.format(tracks) diff --git a/src/clyphx/core/xcomponent.py b/src/clyphx/core/xcomponent.py index d92e977..fc96976 100644 --- a/src/clyphx/core/xcomponent.py +++ b/src/clyphx/core/xcomponent.py @@ -7,7 +7,7 @@ from _Framework.SessionComponent import SessionComponent if TYPE_CHECKING: - from typing import Any + from typing import Any, Text, Union from .live import Track log = logging.getLogger(__name__) @@ -89,11 +89,11 @@ def do_parameter_adjustment(param, value): if len(rnd_range_data) == 2: try: new_min = int(rnd_range_data[0]) - except: + except Exception: new_min = 0 try: new_max = int(rnd_range_data[1]) + 1 - except: + except Exception: new_max = 128 if 0 <= new_min and new_max <= 128 and new_min < new_max: rnd_min = new_min @@ -106,9 +106,9 @@ def do_parameter_adjustment(param, value): if 0 <= int(value) < 128: try: new_value = (int(value) * step) + param.min - except: + except Exception: new_value = param.value - except: + except Exception: pass if param.min <= new_value <= param.max: param.value = new_value @@ -124,7 +124,7 @@ def get_adjustment_factor(string, as_float=False): if len(string) > 1: try: factor = (float if as_float else int)(string[1:]) - except: + except Exception: factor = 1 if string.startswith('<'): diff --git a/src/clyphx/cs_linker.py b/src/clyphx/cs_linker.py index 80c9f00..3d4d51b 100644 --- a/src/clyphx/cs_linker.py +++ b/src/clyphx/cs_linker.py @@ -24,6 +24,7 @@ from typing import Any, Iterable, Sequence, Optional, Dict, Text, List from .core.live import MidiRemoteScript +from pushbase.push_base import PushBase from _Framework.ControlSurface import ControlSurface from .core.xcomponent import ControlSurfaceComponent, SessionComponent @@ -47,7 +48,7 @@ def disconnect(self): for obj in self._slave_objects: if obj is not None: obj.disconnect() - self._slave_objects = None # type: ignore + self._slave_objects = None # type: ignore super().disconnect() def update(self): @@ -102,7 +103,7 @@ def connect_script_instances(self, instantiated_scripts): log.info('Scripts found (%s)', self.canonical_parent) ssn_comps = [] for script in scripts: - if script.__class__.__name__.upper() in ('PUSH', 'PUSH2'): + if isinstance(script, PushBase): ssn_comps.append(script._session_ring) for c in script.components: if isinstance(c, SessionComponent): diff --git a/src/clyphx/instant_doc.py b/src/clyphx/instant_doc.py index 5749fc7..c4fb869 100644 --- a/src/clyphx/instant_doc.py +++ b/src/clyphx/instant_doc.py @@ -20,7 +20,7 @@ import logging if TYPE_CHECKING: - from typing import Any, Iterator, List, Dict, Text + from typing import Any, List, Dict, Text from _Generic.Devices import (DEVICE_DICT, DEVICE_BOB_DICT, BANK_NAME_DICT) from .consts import LIVE_VERSION, DEV_NAME_TRANSLATION diff --git a/src/clyphx/macrobat/parameter_racks.py b/src/clyphx/macrobat/parameter_racks.py index dd2e04a..9a2075f 100644 --- a/src/clyphx/macrobat/parameter_racks.py +++ b/src/clyphx/macrobat/parameter_racks.py @@ -294,7 +294,7 @@ def setup_device(self, rack): if name.startswith('SEND') and self._track != self.song().master_track: try: param = self._track.mixer_device.sends[ord(name[5]) - 65] - except: + except Exception: param = None elif name.startswith('VOL'): param = self._track.mixer_device.volume diff --git a/src/clyphx/triggers/track.py b/src/clyphx/triggers/track.py index 4867e8f..a404b7b 100644 --- a/src/clyphx/triggers/track.py +++ b/src/clyphx/triggers/track.py @@ -86,7 +86,11 @@ def on_timer(self): self._parent._process_xclips_if_track_muted): if self._triggered_clips: for clip in self._triggered_clips: - self._parent.handle_action_list_trigger(self._track, clip) + try: + if clip.name.lstrip()[0] == '[': + self._parent.handle_action_list_trigger(self._track, clip) + except IndexError: + pass self._triggered_clips = [] if self._triggered_lseq_clip: self._parent.handle_loop_seq_action_list(self._triggered_lseq_clip, diff --git a/src/clyphx/user_actions.py b/src/clyphx/user_actions.py index 534bf8b..250a2ef 100644 --- a/src/clyphx/user_actions.py +++ b/src/clyphx/user_actions.py @@ -87,7 +87,7 @@ from .triggers import ActionList if TYPE_CHECKING: - from typing import Any, Text, Dict, List, Optional + from typing import Any, Text, Dict from .live import Track diff --git a/src/clyphx/user_config.py b/src/clyphx/user_config.py index 6315816..c67f518 100644 --- a/src/clyphx/user_config.py +++ b/src/clyphx/user_config.py @@ -131,7 +131,7 @@ def _parse_config(self, path): value = config.get(section, option) getattr(self, section)[option] = value - xcontrols = property(lambda s: getattr(s, 'user_controls', {})) + xcontrols = property(lambda s: getattr(s, 'user_controls', {})) snapshots = property(lambda s: getattr(s, 'snapshot_settings', {})) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 509b355..6b2a9d9 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -2,7 +2,6 @@ # region USER SETTINGS TEST - RESULT = { 'snapshot_settings': { 'include_nested_devices_in_snapshots': True, @@ -43,11 +42,10 @@ def test_user_settings(user_settings): assert cfg.cslinker == RESULT['cslinker'] assert cfg.vars == cfg.user_variables == RESULT['user_variables'] assert cfg.identifier_note == RESULT['identifier_note'] - # endregion -# region USER CONTROLS +# region USER CONTROLS def test_user_controls(): from clyphx.core.models import UserControl, STATUS_BYTE @@ -61,11 +59,10 @@ def test_user_controls(): uc = UserControl.parse(name, data) for attr, val in asserts.items(): assert getattr(uc, attr) == val - # endregion -# region COMMAND PARSER TEST +# region COMMAND PARSER TEST def test_specs(): from clyphx.core.parse import Parser @@ -94,7 +91,7 @@ def test_specs(): for i, value in enumerate(target.get(step, [])): assert getattr(res, step)[i].tracks == value + def test_actions(): pass - -# rendregion +# endregion