diff --git a/.vscode/.settings.json b/.vscode/.settings.json index cb2b6c8..4495abf 100644 --- a/.vscode/.settings.json +++ b/.vscode/.settings.json @@ -22,13 +22,18 @@ "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true }, - "[git-commit]": { + "[properties]": { "editor.rulers": [ - 50 + 80 ] }, "[powershell]": { "files.encoding": "utf16le", "files.eol": "\r\n" + }, + "[git-commit]": { + "editor.rulers": [ + 50 + ] } } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d64c76..0bc20b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,4 +8,5 @@ PS> git clone https://github.com/nuno-andre/clyphx.git PS> cd clyphx PS> . .\tools\win.ps1; install-runtime -``` \ No newline at end of file +PS> python3 .\tools\vscode.py +``` diff --git a/src/clyphx/UserSettings.txt b/src/clyphx/UserSettings.txt index e1510a5..49cb943 100644 --- a/src/clyphx/UserSettings.txt +++ b/src/clyphx/UserSettings.txt @@ -23,14 +23,14 @@ # Please DO NOT change the name of this file or its file extension. When done # making your changes to the settings below, just save the file. -# After saving this file, you will need to close/restart Live for your changes +# After saving this file, you will need to restart Live for your changes # to take effect. -# For Windows 7/Vista users, depending on how your privileges are set up, you may -# not be able to save changes you make to this file. You may receive an error -# such as Access Denied when trying to save. If this occurs, you will need to -# drag this file onto your desktop, then make your changes and save. When -# done, drag the file back into the ClyphX folder. +# For Windows 7/Vista users, depending on how your privileges are set up, you +# may not be able to save changes you make to this file. You may receive an +# error such as Access Denied when trying to save. If this occurs, you will +# need to drag this file onto your desktop, then make your changes and save. +# When done, drag the file back into the ClyphX folder. @@ -58,7 +58,8 @@ SNAPSHOT_PARAMETER_LIMIT = 500 # Note: # Please use caution when adjusting this setting. Recalling Snapshots that have -# stored 1000 or more parameters can cause delays and momentary freezing of Live's GUI. +# stored 1000 or more parameters can cause delays and momentary freezing of +# Live's GUI. @@ -126,9 +127,9 @@ CLIP_RECORD_LENGTH_SET_BY_GLOBAL_QUANTIZATION = Off # Description: # This changes the behavior of launching the selected Clip Slot so that -# (under the Conditions listed below) you can easily record a new Clip with a length -# defined by the Global Quantization value. This will do nothing if the Conditions -# below aren't met. +# (under the Conditions listed below) you can easily record a new Clip with a +# length defined by the Global Quantization value. This will do nothing if the +# Conditions below aren't met. # Conditions: # - Selected Track is armed @@ -142,28 +143,30 @@ DEFAULT_INSERTED_MIDI_CLIP_LENGTH = 0 # 0 (for Off) or 2 - 16 (for number of bars to use) # Description: -# Upon inserting a blank MIDI Clip onto the selected Clip Slot, the Clip's length -# will be set to the length (in bars) specified in the setting above. +# Upon inserting a blank MIDI Clip onto the selected Clip Slot, the Clip's +# length will be set to the length (in bars) specified in the setting above. # Note: -# This will not change the default zoom setting of the Clip, so you'll only see the -# Clip's first bar. You'll need to zoom out to see the rest of the Clip. +# This will not change the default zoom setting of the Clip, so you'll only see +# the Clip's first bar. You'll need to zoom out to see the rest of the Clip. ***************************** [CSLINKER] ************************** -# CsLinker allows you to link the grid selectors (colored borders around clips) of -# two Control Surfaces either horizontally or vertically. +# CsLinker allows you to link the grid selectors (colored borders around clips) +# of two Control Surfaces either horizontally or vertically. -# The Control Surface script names to use are as shown in Live's Control Surface chooser. -# If a Control Surface's name has a space in it (like MXT Live), you should use an underscore -# in place of the space (like MXT_Live). +# The Control Surface script names to use are as shown in Live's Control Surface +# chooser. If a Control Surface's name has a space in it (like MXT Live), you +# should use an underscore in place of the space (like MXT_Live). -# Note, Push and Push2 cannot be used for matched linking. Additionally, horizontal linking -# with Push2 may produce undesirable results due to Push2's inclusion of Chain mixer settings -# along side normal Track mixer settings. +# Note, Push and Push2 cannot be used for matched linking. Additionally, +# horizontal linking with Push2 may produce undesirable results due to Push2's +# inclusion of Chain mixer settings along side normal Track mixer settings. + +# You can also omit 'CSLINKER_' in props names, e.g.: MATCHED_LINK CSLINKER_MATCHED_LINK = False @@ -171,9 +174,9 @@ CSLINKER_MATCHED_LINK = False # True for matched link or False for horizonal/vertical link. # Description: -# Determines whether the two Control Surfaces should have a matched link meaning that -# they will lay on top of each other. This setting overrides CSLINKER_HORIZONTAL_LINK. -# and CSLINKER_MULTI_AXIS_LINK. +# Determines whether the two Control Surfaces should have a matched link meaning +# that they will lay on top of each other. This setting overrides +# CSLINKER_HORIZONTAL_LINK and CSLINKER_MULTI_AXIS_LINK. @@ -182,7 +185,8 @@ CSLINKER_HORIZONTAL_LINK = True # True for horizontal link or False for vertical link. # Description: -# Determines whether the two Control Surfaces should be horizontal or vertically linked. +# Determines whether the two Control Surfaces should be horizontal or vertically +# linked. @@ -190,8 +194,9 @@ CSLINKER_MULTI_AXIS_LINK = False # Setting: # True for multi-axis link. -# Determines whether movement should be sychronized in all directions (vertical and horizontal) or -# purely on a single axis determined by the CS_HORIZONTAL_LINK setting. +# Determines whether movement should be sychronized in all directions (vertical +# and horizontal) or purely on a single axis determined by the +# CS_HORIZONTAL_LINK setting. @@ -218,42 +223,39 @@ CSLINKER_SCRIPT_2_NAME = None # Below, you can specify a list of MIDI Controls to use as X-Controls. -# The entry format is: CONTROL_NAME = MSG_TYPE, MIDI_CHANNEL, NOTE_OR_CC_NUM, ON_ACTION_LIST +# The entry format is: +# CONTROL_NAME = MSG_TYPE, MIDI_CHANNEL, NOTE_OR_CC_NUM, ON_ACTION_LIST -# CONTROL_NAME = A unique one-word name (Identifier) for the control. See [IDENTIFIER NOTE] below. +# CONTROL_NAME = A unique one-word name (Identifier) for the control. +# See [IDENTIFIER NOTE] below. # MSG_TYPE = The word Note or CC. # MIDI_CHANNEL = The MIDI Channel number in the range of 1 - 16 # NOTE_OR_CC = The Note or CC number in the range of 0 - 127. -# ON_ACTION_LIST = The Action List to perform when the control sends an on message. +# ON_ACTION_LIST = The Action List to perform when the control sends an on +# message. # Example: MY_BTN1 = NOTE, 1, 10, 1/MUTE ; 2/MUTE - -# You can optionally specify an Action List to perform when the control sends an off message. -# To do this, place a comma after the On Action List and then specify the Off Action List. +# You can optionally specify an Action List to perform when the control sends +# an off message. To do this, place a comma after the On Action List and then +# specify the Off Action List. # Example: MY_BTN2 = CC, 16, 117, 1/MUTE ; 2/MUTE, 3/PLAY > - -# To perform the same Action List for the On Action List and Off Action List, just specify an asterick -# for the Off Action List. +# To perform the same Action List for the On Action List and Off Action List, +# just specify an asterisk for the Off Action List. # Example: MY_BTN3 = NOTE, 5, 0, 1/MUTE, * - -# Below is an example list that has been commented out (the # at the beginning of -# a line makes the line a comment). Your list should be formatted in the same way -# except without the # at the beginning of each line. +# Below is an example list that has been commented out (the # at the beginning +# of a line makes the line a comment). Your list should be formatted in the same +# way except without the # at the beginning of each line. # btn_1 = note, 1, 0, mute , * - # btn_2 = note, 1, 1, solo - # btn_3 = cc, 9, 2, arm - # btn_4 = cc, 9, 3, mon - #>>>>>>>>DELETE THIS ENTIRE LINE AND START YOUR LIST HERE<<<<<<<<# @@ -264,9 +266,10 @@ CSLINKER_SCRIPT_2_NAME = None # The entry format is: VARIABLE_NAME = VALUE -# VARIABLE_NAME = A unique one-word name (Identifier) for the variable. See [IDENTIFIER NOTE] below. -# VALUE = Any value or word or combination of words. See the User Variables section of the manual for -# more info on this. +# VARIABLE_NAME = A unique one-word name (Identifier) for the variable. +# See [IDENTIFIER NOTE] below. +# VALUE = Any value or word or combination of words. See the User Variables +# section of the manual for more info on this. # The Variables listed below are just examples and can be removed. @@ -275,8 +278,10 @@ ex_var1 = 10 ex_var2 = mute -******************************* [IDENTIFIER NOTE] ******************************* +******************************* [IDENTIFIER NOTE] ****************************** + +# Identifiers and Variable names should not contain characters other than +# letters, numbers and underscores. -# Identifiers and Variable names should not contain characters other than letters, numbers and underscores. # Also, Variable names and their values are not case-sensitive. diff --git a/src/clyphx/actions/control_surface.py b/src/clyphx/actions/control_surface.py index a6f325e..edb21fd 100644 --- a/src/clyphx/actions/control_surface.py +++ b/src/clyphx/actions/control_surface.py @@ -92,9 +92,7 @@ def connect_script_instances(self, instantiated_scripts): if script_name == 'MXT_Live': self._mxt_actions.set_script(script) if not script_name.startswith('ClyphX'): - if script._components is None: - return - else: + if script._components is not None: self._scripts[i]['name'] = script_name.upper() for c in script.components: if isinstance(c, SessionComponent): @@ -110,7 +108,7 @@ def connect_script_instances(self, instantiated_scripts): 'component': None, 'override': None, } - if script_name == 'Launchpad': + elif script_name == 'Launchpad': self._scripts[i]['color'] = { 'GREEN': (52, 56), 'RED': (7, 11), @@ -516,8 +514,8 @@ def on_time_changed(self): (self._override and self._override._mode_index != 1) ): time = str(self.song().get_current_beats_song_time()).split('.') - if self._last_beat != int(time[1])-1: - self._last_beat = int(time[1])-1 + if self._last_beat != int(time[1]) - 1: + self._last_beat = int(time[1]) - 1 self.clear() if self._last_beat < len(self._controls): self._controls[self._last_beat].turn_on() diff --git a/src/clyphx/actions/global_.py b/src/clyphx/actions/global_.py index 771c025..9eeade0 100644 --- a/src/clyphx/actions/global_.py +++ b/src/clyphx/actions/global_.py @@ -284,7 +284,9 @@ def _get_current_preset_index(self, device, presets): '''Returns the index of the current preset (based on the device's name) in the presets list. Returns -1 if not found. ''' - current_preset_name = '{}.{}'.format(device.name, 'adg' if device.can_have_chains else 'adv') + current_preset_name = '{}.{}'.format( + device.name, 'adg' if device.can_have_chains else 'adv' + ) for i in range(len(presets)): if presets[i].name == current_preset_name: @@ -369,7 +371,7 @@ def trigger_session_record(self, track, xclip, ident, value=None): break bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator try: - length = float(value.strip()) * bar + length = float(value) * bar except: length = bar self.song().trigger_session_record(length) diff --git a/src/clyphx/clyphx.py b/src/clyphx/clyphx.py index 9c5fa06..9a9bceb 100644 --- a/src/clyphx/clyphx.py +++ b/src/clyphx/clyphx.py @@ -24,6 +24,7 @@ from _Framework import Task from raven.utils.six import iteritems +from .core import UserSettings from .macrobat import Macrobat from .extra_prefs import ExtraPrefs from .cs_linker import CsLinker @@ -52,6 +53,9 @@ SCRIPT_NAME = 'ClyphX v2.7.0' +# def schedule_message(self, delay_in_ticks, callback, parameter = None): + + class ClyphX(ControlSurface): '''ClyphX Main. ''' @@ -64,9 +68,10 @@ def __init__(self, c_instance): self._push_emulation = False self._PushApcCombiner = None self._process_xclips_if_track_muted = True + self._user_settings = UserSettings() with self.component_guard(): self._macrobat = Macrobat(self) - self._extra_prefs = ExtraPrefs(self) + self._extra_prefs = ExtraPrefs(self, self._user_settings.prefs) self._cs_linker = CsLinker() self._track_actions = XTrackActions(self) self._snap_actions = XSnapActions(self) @@ -283,11 +288,12 @@ def handle_action_list_trigger(self, track, xtrigger): ident, self.track_list_to_string(action['track']), action['action'], action['args']) def get_xclip_action_list(self, xclip, full_action_list): - '''Get the action list to perform. X-Clips can have an on and - off action list separated by a comma. This will return which - action list to perform based on whether the clip is playing. If - the clip is not playing and there is no off action, this returns - None. + '''Get the action list to perform. + + X-Clips can have an on and off action list separated by a comma. + This will return which action list to perform based on whether + the clip is playing. If the clip is not playing and there is no + off action, this returns None. ''' result = None split_list = full_action_list.split(',') @@ -301,37 +307,35 @@ def get_xclip_action_list(self, xclip, full_action_list): log.debug('get_xclip_action_list returning %s', result) return result - def replace_user_variables(self, string_with_vars): + def replace_user_variables(self, string): '''Replace any user variables in the given string with the value the variable represents. ''' - while '%' in string_with_vars: - var = string_with_vars[string_with_vars.index('%')+1:] + while '%' in string: + var = string[string.index('%')+1:] if '%' in var: var = var[0:var.index('%')] - string_with_vars = string_with_vars.replace( + string = string.replace( '%{}%'.format(var), self.get_user_variable_value(var), 1 ) else: - string_with_vars = string_with_vars.replace('%', '', 1) - if '$' in string_with_vars: + string = string.replace('%', '', 1) + if '$' in string: # for compat with old-style variables - for string in string_with_vars.split(): - if '$' in string and not '=' in string: - var = string.replace('$', '') - string_with_vars = string_with_vars.replace( + for s in string.split(): + if '$' in s and not '=' in s: + var = s.replace('$', '') + string = string.replace( '${}'.format(var), self.get_user_variable_value(var), 1 ) - log.debug('replace_user_variables returning %s', string_with_vars) - return string_with_vars + log.debug('replace_user_variables returning %s', string) + return string def get_user_variable_value(self, var): '''Get the value of the given variable name or 0 if var name not found. ''' - result = '0' - if var in self._user_variables: - result = self._user_variables[var] + result = self._user_variables.get(var, '0') log.debug('get_user_variable_value returning %s=%s', var, result) return result @@ -556,7 +560,7 @@ def get_device_to_operate_on(self, track, action_name, args): if 'DEV"' in action_name: dev_name = action_name[action_name.index('"')+1:] if '"' in args: - dev_name = action_name[action_name.index('"')+1:] + ' ' + args + dev_name = '{} {}'.format(action_name[action_name.index('"')+1:], args) device_args = args[args.index('"')+1:].strip() if '"' in dev_name: dev_name = dev_name[0:dev_name.index('"')] @@ -564,26 +568,25 @@ def get_device_to_operate_on(self, track, action_name, args): if dev.name.upper() == dev_name: device = dev break + elif action_name == 'DEV': + device = track.view.selected_device + if device is None: + if track.devices: + device = track.devices[0] else: - if action_name == 'DEV': - device = track.view.selected_device - if device is None: - if track.devices: - device = track.devices[0] - else: - try: - dev_num = action_name.replace('DEV', '') - if '.' in dev_num and self._can_have_nested_devices: - dev_split = dev_num.split('.') - top_level = track.devices[int(dev_split[0]) - 1] - if top_level and top_level.can_have_chains: - device = top_level.chains[int(dev_split[1]) - 1].devices[0] - if len(dev_split) > 2: - device = top_level.chains[int(dev_split[1]) - 1].devices[int(dev_split[2]) - 1] - else: - device = track.devices[int(dev_num) - 1] - except: - pass + try: + dev_num = action_name.replace('DEV', '') + if '.' in dev_num and self._can_have_nested_devices: + dev_split = dev_num.split('.') + top_level = track.devices[int(dev_split[0]) - 1] + if top_level and top_level.can_have_chains: + device = top_level.chains[int(dev_split[1]) - 1].devices[0] + if len(dev_split) > 2: + device = top_level.chains[int(dev_split[1]) - 1].devices[int(dev_split[2]) - 1] + else: + device = track.devices[int(dev_num) - 1] + except: + pass log.debug('get_device_to_operate_on returning device=%s and device args=%s', device.name if device else 'None', device_args) return (device, device_args) @@ -632,7 +635,7 @@ def get_drum_rack_to_operate_on(self, track): dr = device break log.debug('get_drum_rack_to_operate_on returning dr=%s', - dr.name if dr else 'None') + dr.name if dr else None) return dr def get_user_settings(self, midi_map_handle): @@ -643,14 +646,13 @@ def get_user_settings(self, midi_map_handle): list_to_build = None ctrl_data = [] - prefs_data = [] try: mrs_path = '' for path in sys.path: if 'MIDI Remote Scripts' in path: mrs_path = path break - user_file = mrs_path + FOLDER + 'UserSettings.txt' + user_file = '{}{}UserSettings.txt'.format(mrs_path, FOLDER) if not self._user_settings_logged: log.info('Attempting to read UserSettings file: %s', user_file) for line in open(user_file): @@ -675,8 +677,6 @@ def get_user_settings(self, midi_map_handle): self.handle_user_variable_assignment(line) elif list_to_build == 'controls': ctrl_data.append(line) - elif list_to_build == 'prefs': - prefs_data.append(line) elif 'PUSH_EMULATION' in line: self._push_emulation = line.split('=')[1].strip() == 'ON' if self._push_emulation: @@ -686,10 +686,7 @@ def get_user_settings(self, midi_map_handle): self.enable_push_emulation(self._control_surfaces()) elif line.startswith('INCLUDE_NESTED_DEVICES_IN_SNAPSHOTS ='): include_nested = self.get_name(line[37:].strip()) - include_nested_devices = False - if include_nested.startswith('ON'): - include_nested_devices = True - self._snap_actions._include_nested_devices = include_nested_devices + self._snap_actions._include_nested_devices = include_nested.startswith('ON') elif line.startswith('SNAPSHOT_PARAMETER_LIMIT ='): try: limit = int(line[26:].strip()) @@ -708,8 +705,6 @@ def get_user_settings(self, midi_map_handle): self._cs_linker.parse_settings(line) if ctrl_data: self._control_component.get_user_control_settings(ctrl_data, midi_map_handle) - if prefs_data: - self._extra_prefs.get_user_settings(prefs_data) except: pass @@ -756,11 +751,12 @@ def track_list_to_string(self, track_list): '''Convert list of tracks to a string of track names or None if no tracks. This is used for debugging. ''' + # TODO: if no track_list result is 'None]' result = 'None' if track_list: result = '[' for track in track_list: - result += track.name + ', ' + result += '{}, '.format(track.name) result = result[:len(result)-2] return result + ']' @@ -777,9 +773,7 @@ def setup_tracks(self): ''' for t in self.song().tracks: self._macrobat.setup_tracks(t) - if self._current_tracks and t in self._current_tracks: - pass - else: + if not (self._current_tracks and t in self._current_tracks): self._current_tracks.append(t) XTrackComponent(self, t) for r in chain(self.song().return_tracks, (self.song().master_track,)): diff --git a/src/clyphx/core.py b/src/clyphx/core.py index c79b418..e415e46 100644 --- a/src/clyphx/core.py +++ b/src/clyphx/core.py @@ -1,9 +1,11 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, with_statement import re +import os +import logging from collections import namedtuple -from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +log = logging.getLogger(__name__) Action = namedtuple('Action', ['track', 'actions', 'args']) @@ -18,6 +20,9 @@ class Parser(object): r'(\s*?,\s*?(?P\S[^,]*?))?\s*?$') actions = re.compile(r'^\s+|\s*;\s*|\s+$') + def __init__(self): + self._user_stettings = dict() + def __call__(self, text): cmd = self.command.search(text).groupdict() lists = self.lists.match(cmd.pop('actions')).groupdict() @@ -30,6 +35,154 @@ def __call__(self, text): return Command(**cmd) +class UserControl(object): + ''' + + Args: + - name (str): A unique one-word identifier for the control. + - type (str): Message type: 'NOTE' or 'CC'. + - channel (str): MIDI channel (1 - 16). + - value (str): Note or CC value (0 - 127). + - actions (str, optional): Action List to perform when the control + sends an on. + ''' + __slots__ = ('name', 'type', 'channel', 'value', 'actions') + + def __init__(self, name, type, channel, value, actions=None): + self.name = name + self.type = type.lower() + self.channel = int(channel) + self.value = int(value) + # TODO: parse actions + self.actions = actions + self._validate() + + def _validate(self): + # TODO: check valid identifier + + if self.type not in {'note', 'cc'}: + raise ValueError("Message type must be 'NOTE' or 'CC'") + + if not (1 <= self.channel <= 16): + raise ValueError('MIDI channel must be an integer between 1 and 16') + + if not (0 <= self.value <= 127): + raise ValueError('NOTE or CC must be an integer between 0 and 127') + + def __repr__(self): + # TODO: import from utils + return '{}({})'.format( + type(self).__name__, + ', '.join('{0}={1}'.format(k, getattr(self, k)) + for k in self.__slots__), + ) + + +class UserSettings(object): + def __init__(self): + self.vars = dict() + self.controls = dict() + self.prefs = dict() + self.snapshots = dict() + self.cs_linker = dict() + self._parse_file() + + def _parse_file(self): + SECTIONS = r'^\*+?\s?([^\*]*?)\s?\*+\s*?$((?!^\*).*?)(?=\n\*|\Z)' + CONTENT = r'^(?! )[^\*#\n"]+$' + METHODS = { + '[USER VARIABLES]': self._user_vars, + '[USER CONTROLS]': self._user_controls, + '[EXTRA PREFS]': self._extra_prefs, + '[SNAPSHOT SETTINGS]': self._snapshot_settings, + '[CSLINKER]': self._cs_linker, + } + + folder = os.path.dirname(os.path.realpath(__file__)) + filepath = os.path.join(folder, 'UserSettings.txt') + + for m in re.findall(SECTIONS, open(filepath).read(), re.M | re.S): + section, content = m + try: + method = METHODS[section] + method(re.findall(CONTENT, content, re.M)) + except KeyError: + pass + + def _split_lines(self, content, lower=True): + for line in map(str.lower if lower else str, content): + try: + yield tuple(x.strip() for x in line.split('=')) + except Exception as e: + log.error('Error parsing setting: %s (%s)', line, e) + + def _user_vars(self, content): + # TODO + self.vars = {k: v for k, v in self._split_lines(content, False)} + + def _user_controls(self, content): + CONTROL = re.compile(r'^(?P[^,]*?)\s*?,\s*?' + r'(?P(?! )[\d]*)\s*?,\s*?' + r'(?P(?! )[\d]*)\s*?' + r'([,;]\s*?(?P(?! ).*))?') + + # TODO: don't split line and parse the name + for (k, v) in self._split_lines(content): + try: + # TODO: parse actions + res = CONTROL.match(v).groupdict() + res.update(name=k) + self.controls[k] = UserControl(**res) + except Exception as e: + log.exception(e) + + def _extra_prefs(self, content): + for (k, v) in self._split_lines(content): + if k in {'navigation_highlight', + 'exclusive_arm_on_select', + 'exclusive_show_group_on_select', + 'clip_record_length_set_by_global_quantization'}: + self.prefs[k] = v == 'on' + elif k == 'process_xclips_if_track_muted': + self.prefs[k] = v == 'true' + elif k == 'startup_actions': + self.prefs[k] = '[]{}'.format(v) if v != 'off' else None + elif k == 'default_inserted_midi_clip_length': + self.prefs[k] = False + try: + if 2 <= int(v) < 17: + self.prefs[k] = int(v) + except: + pass + else: + log.warning('Extra preference unknown: %s = %s', k, v) + + def _snapshot_settings(self, content): + for (k, v) in self._split_lines(content): + if k == 'include_nested_devices_in_snapshots': + self.snapshots['include_nested_devices'] = v == 'on' + elif k == 'snapshot_parameter_limit': + try: + self.snapshots['parameter_limit'] = int(v) + except: + # TODO: initialize defaults? + self.snapshots['parameter_limit'] = 500 + else: + log.warning('Snapshot setting unknown: %s = %s', k, v) + + def _cs_linker(self, content): + for (k, v) in self._split_lines(content): + k = '_{}'.format(k.replace('cslinker_', '')) + if k in {'_matched_link', '_horizontal_link', '_multi_axis_link'}: + self.cs_linker[k] = v == 'true' + elif k in {'_script_1_name', '_script_2_name'}: + self.cs_linker[k] = v if v != 'none' else None + else: + log.warning('CS Linker setting unknown: %s = %s', k, v) + + +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + class XComponent(ControlSurfaceComponent): '''Control Surface base component. ''' diff --git a/src/clyphx/cs_linker.py b/src/clyphx/cs_linker.py index 6c63146..cbde4fa 100644 --- a/src/clyphx/cs_linker.py +++ b/src/clyphx/cs_linker.py @@ -30,7 +30,7 @@ class CsLinker(ControlSurfaceComponent): ''' def __init__(self): - ControlSurfaceComponent.__init__(self) + super(CsLinker, self).__init__() self._slave_objects = [None, None] self._script_names = None self._horizontal_link = False @@ -43,7 +43,7 @@ def disconnect(self): if obj: obj.disconnect() self._slave_objects = None - ControlSurfaceComponent.disconnect(self) + super(CsLinker, self).disconnect() def update(self): pass @@ -78,7 +78,6 @@ def connect_script_instances(self, instantiated_scripts): ''' if self._script_names: scripts = [None, None] - found_scripts = False scripts_have_same_name = self._script_names[0] == self._script_names[1] for script in instantiated_scripts: script_name = script.__class__.__name__.upper() @@ -92,51 +91,51 @@ def connect_script_instances(self, instantiated_scripts): scripts[0] = script elif script_name == self._script_names[1]: scripts[1] = script - found_scripts = scripts[0] and scripts[1] - if found_scripts: + if scripts[0] and scripts[1]: break - if found_scripts: - log.info('Scripts found (%s)', self.canonical_parent) - ssn_comps = [] - for script in scripts: - if script.__class__.__name__.upper() in ('PUSH', 'PUSH2'): - ssn_comps.append(script._session_ring) - for c in script.components: - if isinstance (c, SessionComponent): - ssn_comps.append(c) - break - if len(ssn_comps) == 2: - log.info('SessionComponents for specified scripts located (%s)', - self.canonical_parent) - if self._matched_link: - for s in ssn_comps: - s._link() - else: - if self._script_names[0] in ('PUSH', 'PUSH2'): - h_offset = ssn_comps[0].num_tracks - v_offset = ssn_comps[0].num_scenes - else: - h_offset = ssn_comps[0].width() - v_offset = ssn_comps[0].height() - h_offset_1 = 0 if not self._horizontal_link and self._multi_axis_link else -(h_offset) - v_offset_1 = 0 if self._horizontal_link and self._multi_axis_link else -(v_offset) - h_offset_2 = 0 if not self._horizontal_link and self._multi_axis_link else h_offset - v_offset_2 = 0 if self._horizontal_link and self._multi_axis_link else v_offset - self._slave_objects[0] = SessionSlave( - self._horizontal_link, self._multi_axis_link, - ssn_comps[0], ssn_comps[1], h_offset_1, v_offset_1, - ) - self._slave_objects[1] = SessionSlaveSecondary( - self._horizontal_link, self._multi_axis_link, - ssn_comps[1], ssn_comps[0], h_offset_2, v_offset_2, - ) - self.canonical_parent.schedule_message(10, self._refresh_slave_objects) - else: - log.error('Unable to locate SessionComponents for specified scripts (%s)', - self.canonical_parent) else: log.error('Unable to locate specified scripts (%s)', self.canonical_parent) + return + + log.info('Scripts found (%s)', self.canonical_parent) + ssn_comps = [] + for script in scripts: + if script.__class__.__name__.upper() in ('PUSH', 'PUSH2'): + ssn_comps.append(script._session_ring) + for c in script.components: + if isinstance(c, SessionComponent): + ssn_comps.append(c) + break + if len(ssn_comps) == 2: + log.info('SessionComponents for specified scripts located (%s)', + self.canonical_parent) + if self._matched_link: + for s in ssn_comps: + s._link() + else: + if self._script_names[0] in ('PUSH', 'PUSH2'): + h_offset = ssn_comps[0].num_tracks + v_offset = ssn_comps[0].num_scenes + else: + h_offset = ssn_comps[0].width() + v_offset = ssn_comps[0].height() + h_offset_1 = 0 if not self._horizontal_link and self._multi_axis_link else -(h_offset) + v_offset_1 = 0 if self._horizontal_link and self._multi_axis_link else -(v_offset) + h_offset_2 = 0 if not self._horizontal_link and self._multi_axis_link else h_offset + v_offset_2 = 0 if self._horizontal_link and self._multi_axis_link else v_offset + self._slave_objects[0] = SessionSlave( + self._horizontal_link, self._multi_axis_link, + ssn_comps[0], ssn_comps[1], h_offset_1, v_offset_1, + ) + self._slave_objects[1] = SessionSlaveSecondary( + self._horizontal_link, self._multi_axis_link, + ssn_comps[1], ssn_comps[0], h_offset_2, v_offset_2, + ) + self.canonical_parent.schedule_message(10, self._refresh_slave_objects) + else: + log.error('Unable to locate SessionComponents for specified scripts (%s)', + self.canonical_parent) def on_track_list_changed(self): '''Refreshes slave objects if horizontally linked.''' @@ -155,9 +154,8 @@ def _refresh_slave_objects(self): obj._on_offsets_changed() - class SessionSlave(object): - '''SessionSlave is the base class for linking two SessionComponents. + '''Base class for linking two SessionComponents. ''' def __init__(self, horz_link, multi_axis, self_comp, observed_comp, h_offset, v_offset): @@ -206,6 +204,7 @@ def _on_offsets_changed(self, arg_a=None, arg_b=None): self._self_scene_offset()) else: return + if not self._horizontal_link or self._multi_axis_link: if callable(self._self_ssn_comp.song): new_num_scenes = len(self._self_ssn_comp.song().scenes) @@ -233,27 +232,32 @@ def _on_offsets_changed(self, arg_a=None, arg_b=None): return def _observed_track_offset(self): - if callable(self._observed_ssn_comp.track_offset): + try: return self._observed_ssn_comp.track_offset() - return self._observed_ssn_comp.track_offset + except TypeError: + return self._observed_ssn_comp.track_offset def _self_track_offset(self): - if callable(self._self_ssn_comp.track_offset): + try: return self._self_ssn_comp.track_offset() - return self._self_ssn_comp.track_offset + except TypeError: + return self._self_ssn_comp.track_offset def _observed_scene_offset(self): - if callable(self._observed_ssn_comp.scene_offset): + try: return self._observed_ssn_comp.scene_offset() - return self._observed_ssn_comp.scene_offset + except TypeError: + return self._observed_ssn_comp.scene_offset def _self_scene_offset(self): - if callable(self._self_ssn_comp.scene_offset): + try: return self._self_ssn_comp.scene_offset() - return self._self_ssn_comp.scene_offset + except TypeError: + return self._self_ssn_comp.scene_offset def _track_offset_change_possible(self): - '''Returns whether or not moving the track offset is possible.''' + '''Returns whether or not moving the track offset is possible. + ''' try: w = self._self_ssn_comp.width() except AttributeError: @@ -265,7 +269,8 @@ def _min_track_offset(self): return 0 def _scene_offset_change_possible(self): - '''Returns whether or not moving the scene offset is possible.''' + '''Returns whether or not moving the scene offset is possible. + ''' try: h = self._self_ssn_comp.height() except AttributeError: @@ -278,10 +283,11 @@ def _min_scene_offset(self): class SessionSlaveSecondary(SessionSlave): - '''SessionSlaveSecondary is the second of the two linked slave objects. + '''SessionSlaveSecondary is the second of the two linked slave + objects. - This overrides the functions that return whether offsets can be changed - as well as the functions that return minimum offsets. + This overrides the functions that return whether offsets can be + changed as well as the functions that return minimum offsets. ''' def _track_offset_change_possible(self): diff --git a/src/clyphx/extra_prefs.py b/src/clyphx/extra_prefs.py index e4d0b03..ebccddf 100644 --- a/src/clyphx/extra_prefs.py +++ b/src/clyphx/extra_prefs.py @@ -16,24 +16,27 @@ from __future__ import absolute_import, unicode_literals +import logging from functools import partial import Live from .core import XComponent +log = logging.getLogger(__name__) + class ExtraPrefs(XComponent): '''Extra prefs component for ClyphX. ''' __module__ = __name__ - def __init__(self, parent): + def __init__(self, parent, config): super(ExtraPrefs, self).__init__(parent) - self._show_highlight = True - self._exclusive_arm = False - self._exclusive_fold = False - self._clip_record = False + self._show_highlight = config.get('navigation_highlight', True) + self._exclusive_arm = config.get('exclusive_arm_on_select', False) + self._exclusive_fold = config.get('exclusive_show_group_on_select', False) + self._clip_record = config.get('clip_record_length_set_by_global_quantization', False) self._clip_record_slot = None - self._midi_clip_length = False + self._midi_clip_length = config.get('default_inserted_midi_clip_length', False) self._midi_clip_length_slot = None self._last_track = self.song().view.selected_track self.on_selected_track_changed() @@ -45,28 +48,6 @@ def disconnect(self): self._midi_clip_length_slot = None super(ExtraPrefs, self).disconnect() - def get_user_settings(self, data): - '''Get user settings from config file and make sure they are in - proper range. - ''' - for d in data: - k, v = d.split('=') - if 'NAVIGATION_HIGHLIGHT' in k: - self._show_highlight = 'ON' in v - elif 'EXCLUSIVE_ARM_ON_SELECT' in k and 'ON' in v: - self._exclusive_arm = True - elif 'EXCLUSIVE_SHOW_GROUP_ON_SELECT' in k and 'ON' in v: - self._exclusive_fold = True - elif 'CLIP_RECORD_LENGTH_SET_BY_GLOBAL_QUANTIZATION' in k and 'ON' in v: - self._clip_record = True - elif 'DEFAULT_INSERTED_MIDI_CLIP_LENGTH' in k: - try: - if 2 <= int(v) < 17: - self._midi_clip_length = int(v) - except: - pass - self.on_selected_track_changed() - def on_selected_track_changed(self): '''Handles navigation highlight, triggering exclusive arm/fold functions and removes/sets up listeners for clip-related @@ -95,13 +76,21 @@ def on_selected_track_changed(self): if self._clip_record: if track.can_be_armed and not clip_slot.has_clip: self._clip_record_slot = clip_slot - if not self._clip_record_slot.has_clip_has_listener(self.clip_record_slot_changed): - self._clip_record_slot.add_has_clip_listener(self.clip_record_slot_changed) + if not self._clip_record_slot.has_clip_has_listener( + self.clip_record_slot_changed + ): + self._clip_record_slot.add_has_clip_listener( + self.clip_record_slot_changed + ) if self._midi_clip_length: if track.has_midi_input and not clip_slot.has_clip and not track.is_foldable: self._midi_clip_length_slot = clip_slot - if not self._midi_clip_length_slot.has_clip_has_listener(self.midi_clip_length_slot_changed): - self._midi_clip_length_slot.add_has_clip_listener(self.midi_clip_length_slot_changed) + if not self._midi_clip_length_slot.has_clip_has_listener( + self.midi_clip_length_slot_changed + ): + self._midi_clip_length_slot.add_has_clip_listener( + self.midi_clip_length_slot_changed + ) self._last_track = track def do_exclusive_fold(self, track): @@ -160,13 +149,21 @@ def do_midi_clip_set_length(self, clip_params): def remove_listeners(self): '''Remove parameter listeners.''' if self._clip_record_slot: - if self._clip_record_slot.has_clip_has_listener(self.clip_record_slot_changed): - self._clip_record_slot.remove_has_clip_listener(self.clip_record_slot_changed) + if self._clip_record_slot.has_clip_has_listener( + self.clip_record_slot_changed + ): + self._clip_record_slot.remove_has_clip_listener( + self.clip_record_slot_changed + ) self._clip_record_slot = None if self._midi_clip_length_slot: - if self._midi_clip_length_slot.has_clip_has_listener(self.midi_clip_length_slot_changed): - self._midi_clip_length_slot.remove_has_clip_listener(self.midi_clip_length_slot_changed) + if self._midi_clip_length_slot.has_clip_has_listener( + self.midi_clip_length_slot_changed + ): + self._midi_clip_length_slot.remove_has_clip_listener( + self.midi_clip_length_slot_changed + ) self._midi_clip_length_slot = None def on_selected_scene_changed(self): diff --git a/src/clyphx/instant_mapping_make_doc.py b/src/clyphx/instant_mapping_make_doc.py index 2ce797b..898005c 100644 --- a/src/clyphx/instant_mapping_make_doc.py +++ b/src/clyphx/instant_mapping_make_doc.py @@ -78,8 +78,9 @@ def __init__(self): log.info('InstantMappingMakeDoc finished.') def _collect_device_infos(self): - '''Returns a dict of dicts for each device containing its friendly - name, bob parameters and bank names/bank parameters if applicable. + '''Returns a dict of dicts for each device containing its + friendly name, bob parameters and bank names/bank parameters if + applicable. ''' dev_dict = {} for k, v in DEVICE_DICT.iteritems(): diff --git a/src/clyphx/m4l_browser.py b/src/clyphx/m4l_browser.py index b30b087..9ac22d3 100644 --- a/src/clyphx/m4l_browser.py +++ b/src/clyphx/m4l_browser.py @@ -23,8 +23,8 @@ class XM4LBrowserInterface(XComponent): - '''XM4LBrowserInterface provides access to browser data and - methods for use in M4L devices. + '''XM4LBrowserInterface provides access to browser data and methods + for use in M4L devices. NOTE: Lazy initialization is used, get_browser_tags method needs to be called first in order to use other methods. @@ -130,7 +130,8 @@ def get_items_for_device(self, device_name): return sorted(self._selected_device['folders'].keys()) + sorted(self._selected_device['items']) def get_items_for_folder(self, folder_name): - '''Returns the list of items in the given folder and stores the folder. + '''Returns the list of items in the given folder and stores the + folder. ''' self._selected_folder = self._selected_device['folders'][folder_name] return sorted(self._selected_folder) diff --git a/src/clyphx/macrobat/macrobat.py b/src/clyphx/macrobat/macrobat.py index 1bffbb9..3381502 100644 --- a/src/clyphx/macrobat/macrobat.py +++ b/src/clyphx/macrobat/macrobat.py @@ -19,7 +19,7 @@ import Live from ..core import XComponent from .midi_rack import MacrobatMidiRack -from .rn_r_rack import MacrobatRnRRack +from .rnr_rack import MacrobatRnRRack from .sidechain_rack import MacrobatSidechainRack from .parameter_racks import ( MacrobatLearnRack, MacrobatChainMixRack, MacrobatDRMultiRack, @@ -99,7 +99,8 @@ def remove_listeners(self): self._current_devices = [] def get_devices(self, dev_list): - '''Go through device and chain lists and setup Macrobat racks.''' + '''Go through device and chain lists and setup Macrobat racks. + ''' for d in dev_list: self.setup_macrobat_rack(d) if not d.name_has_listener(self.setup_devices): @@ -130,7 +131,9 @@ def setup_macrobat_rack(self, rack): m = MacrobatChainMixRack(self._parent, rack, self._track) elif name.startswith('NK DR'): m = MacrobatDRRack(self._parent, rack, self._track) - elif name.startswith('NK LEARN') and self._track == self.song().master_track and not self._has_learn_rack: + elif (name.startswith('NK LEARN') and + self._track == self.song().master_track and + not self._has_learn_rack): m = MacrobatLearnRack(self._parent, rack, self._track) self._has_learn_rack = True elif name.startswith('NK MIDI'): diff --git a/src/clyphx/macrobat/parameter_racks.py b/src/clyphx/macrobat/parameter_racks.py index d49812d..bbb081d 100644 --- a/src/clyphx/macrobat/parameter_racks.py +++ b/src/clyphx/macrobat/parameter_racks.py @@ -50,15 +50,18 @@ def setup_device(self, rack): '''Set up macro 1 and learned param.''' super(MacrobatLearnRack, self).setup_device(rack) self._rack = rack - param = self.song().view.selected_parameter - if 0 in LAST_PARAM: + + try: param = LAST_PARAM[0] + except KeyError: + param = self.song().view.selected_parameter + if self._rack and param: if self._rack.parameters[1].is_enabled and param.is_enabled: index = 1 - m_listener = lambda index = index:self.macro_changed(index) + m_listener = lambda i=index: self.macro_changed(i) self._rack.parameters[1].add_value_listener(m_listener) - p_listener = lambda index = index:self.param_changed(index) + p_listener = lambda i=index: self.param_changed(i) param.add_value_listener(p_listener) self._param_macros[index] = (self._rack.parameters[1], param) self._tasks.add(self.get_initial_value) diff --git a/src/clyphx/macrobat/rn_r_rack.py b/src/clyphx/macrobat/rnr_rack.py similarity index 100% rename from src/clyphx/macrobat/rn_r_rack.py rename to src/clyphx/macrobat/rnr_rack.py diff --git a/src/clyphx/utils.py b/src/clyphx/utils.py index 35a6980..69cd183 100644 --- a/src/clyphx/utils.py +++ b/src/clyphx/utils.py @@ -27,3 +27,11 @@ def get_python_info(serialize=True): } return json.dumps(info, indent=4) if serialize else info + + +def repr(self): + return '{}({})'.format( + type(self).__name__, + ', '.join('{}={}'.format(k, getattr(self, k)) + for k in self.__slots__), + )