diff --git a/README.md b/README.md
index 651f661..63749d6 100644
--- a/README.md
+++ b/README.md
@@ -5,21 +5,21 @@ ClyphX
provides an extensive list of _Actions_ related to controlling different aspects
of Live.
-These _Actions_ can be accessed via _X-Triggers_ (Session View Clips, Arrange
-View Locators or MIDI Controls). Each _X-Trigger_ can trigger either a single
-_Action_ or a list of _Actions_.
-
As an example, a simple _Action_ might toggle Overdub on/off:
```
-OVER
+over
```
A more complex _Action List_ might mute Tracks 1-2, unmute and arm Track 4, turn
the 2nd Device on Track 4 on, and launch the selected Clip on track 4:
```
-1-2/MUTE ON; 4/MUTE OFF; 4/ARM ON; 4/DEV2 ON; 4/PLAY
+1-2/mute on; 4/mute off; 4/arm on; 4/dev2 on; 4/play
```
+These _Actions_ can be accessed via _X-Triggers_ (Session View Clips, Arrange
+View Locators or MIDI Controls). Each _X-Trigger_ can trigger either a single
+_Action_ or a list of _Actions_.
+
**ClyphX** also includes:
- A component called **Macrobat** that adds new functionality to Racks in Live
diff --git a/src/clyphx/actions/arsenal.py b/src/clyphx/actions/arsenal.py
index 8ae97e1..32a2604 100644
--- a/src/clyphx/actions/arsenal.py
+++ b/src/clyphx/actions/arsenal.py
@@ -135,7 +135,8 @@ def _handle_mode_action(self, script, spec):
if mc:
adjust_property(mc, 'selected_mode_index', 0, mc.num_modes - 1, spec[1:])
- def _handle_lock_action(self, script, spec):
+ @staticmethod
+ def _handle_lock_action(script, spec):
# type: (Any, Text) -> None
'''Handles toggling the locking of the current track or
mode-specific locks.
@@ -182,7 +183,8 @@ def _handle_scale_action(self, script, spec, xclip, ident):
self._toggle_scale_offset(scl, value)
scl._notify_scale_settings()
- def _capture_scale_settings(self, script, xclip, ident):
+ @staticmethod
+ def _capture_scale_settings(script, xclip, ident):
# type: (Any, Clip, Text) -> None
'''Captures the current scale type, tonic, in key state, offset
and orientation and adds them to the given xclip's name.
@@ -195,7 +197,8 @@ def _capture_scale_settings(self, script, xclip, ident):
comp.orientation_is_horizontal,
)
- def _recall_scale_settings(self, comp, spec):
+ @staticmethod
+ def _recall_scale_settings(comp, spec):
# type: (Any, Text) -> None
'''Recalls previously stored scale settings.'''
if len(spec) >= 5:
@@ -209,14 +212,15 @@ def _recall_scale_settings(self, comp, spec):
# deprecated
if len(spec) == 5:
value = ['ON'] if spec[4].strip() == 'TRUE' else ['OFF']
- self._toggle_scale_offset(comp, value)
+ XArsenalActions._toggle_scale_offset(comp, value)
else:
offset = parse_int(spec[4], None, 0, comp._offsets.num_pages - 1)
if offset is not None:
comp._offsets.set_page_index(offset)
comp._orientation_is_horizontal = spec[5].strip() == 'TRUE'
- def _toggle_scale_offset(self, comp, arg):
+ @staticmethod
+ def _toggle_scale_offset(comp, arg):
# type: (Any, Sequence[Text]) -> None
'''Toggles between sequent and 4ths offsets.
diff --git a/src/clyphx/actions/clip.py b/src/clyphx/actions/clip.py
index c66c97a..bdaa7fb 100644
--- a/src/clyphx/actions/clip.py
+++ b/src/clyphx/actions/clip.py
@@ -26,7 +26,7 @@
from ..core.legacy import _DispatchCommand
from ..core.xcomponent import XComponent
-from ..core.live import Clip
+from ..core.live import Clip, Conversions
from .clip_env_capture import XClipEnvCapture
from .clip_notes import ClipNotesMixin
from ..consts import (CLIP_GRID_STATES, R_QNTZ_STATES,
@@ -49,7 +49,6 @@ def __init__(self, parent):
def dispatch_actions(self, cmd):
# type: (_DispatchCommand) -> None
from .consts import CLIP_ACTIONS
-
for scmd in cmd:
# TODO: compare with dispatch_device_actions
if scmd.track in self._parent.song().tracks:
@@ -61,17 +60,56 @@ def dispatch_actions(self, cmd):
clip_args = action[1]
if clip_args and clip_args.split()[0] in CLIP_ACTIONS:
func = CLIP_ACTIONS[clip_args.split()[0]]
- func(self, action[0], scmd.track, scmd.xclip, scmd.ident,
+ 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'):
self.dispatch_clip_note_action(action[0], cmd.args)
elif cmd.action_name.startswith('CLIP'):
- self.set_clip_on_off(action[0], scmd.track, scmd.xclip, None, cmd.args)
+ self.set_clip_on_off(action[0], scmd.track, scmd.xclip, cmd.args)
def get_clip_to_operate_on(self, track, action_name, args):
# type: (Track, Text, Text) -> Tuple[Optional[Clip], Text]
'''Get clip to operate on and action to perform with args.'''
+ try:
+ # TODO: add args to parsing
+ parsed = self._parent.parse_obj('clip', action_name)
+ except Exception:
+ try:
+ parsed, args = self._parse_split_name(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'])
+ 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
+ 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
+ return clip, args
+
+ @staticmethod
+ def get_clip_by_name(track, name):
+ # TODO: remove uppers
+ name = name.upper()
+ for slot in track.clip_slots:
+ if slot.has_clip and slot.clip.name.upper() == name:
+ return slot.clip
+
+ @staticmethod
+ def _parse_split_name(action_name, args):
+ clip_name = None
clip_args = args
if 'CLIP"' in action_name:
clip_name = action_name[action_name.index('"')+1:]
@@ -80,51 +118,32 @@ def get_clip_to_operate_on(self, track, action_name, args):
clip_args = args[args.index('"')+1:].strip()
if '"' in clip_name:
clip_name = clip_name[0:clip_name.index('"')]
- for slot in track.clip_slots:
- if slot.has_clip and slot.clip.name.upper() == clip_name:
- clip = slot.clip
- break
- else:
- sel_slot_idx = list(self.song().scenes).index(self.song().view.selected_scene)
- slot_idx = sel_slot_idx
- if action_name == 'CLIP':
- if track.playing_slot_index >= 0:
- slot_idx = track.playing_slot_index
- elif action_name == 'CLIPSEL':
- if self.application().view.is_view_visible('Arranger'):
- clip = self.song().view.detail_clip
- else:
- try:
- slot_idx = int(action_name.replace('CLIP', ''))-1
- except:
- slot_idx = sel_slot_idx
- if clip != None and track.clip_slots[slot_idx].has_clip:
- clip = track.clip_slots[slot_idx].clip
- log.debug('get_clip_to_operate_on -> clip=%s, clip args=%s',
- clip.name if clip else 'None', clip_args)
- return (clip, clip_args)
- def set_clip_name(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ if clip_name is not None:
+ return {'name': clip_name}, clip_args
+ raise ValueError('{} {}'.format(action_name, args))
+
+ def set_clip_name(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Set clip's name.'''
args = args.strip()
if args:
clip.name = args
- def set_clip_on_off(self, clip, track, xclip, ident, value=None):
- # type: (Clip, None, None, None, Optional[Text]) -> None
+ def set_clip_on_off(self, clip, track, xclip, value=None):
+ # type: (Clip, None, None, Optional[Text]) -> None
'''Toggles or turns clip on/off.'''
# XXX: reversed value, not fallback
clip.muted = not KEYWORDS.get(value, clip.muted)
- def set_warp(self, clip, track, xclip, ident, value=None):
- # type: (Clip, None, None, None, Optional[Text]) -> None
+ def set_warp(self, clip, track, xclip, value=None):
+ # type: (Clip, None, None, Optional[Text]) -> None
'''Toggles or turns clip warp on/off.'''
if clip.is_audio_clip:
switch(clip, 'warping', value)
- def adjust_time_signature(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_time_signature(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust clip's time signature.'''
if '/' in args:
try:
@@ -134,13 +153,13 @@ def adjust_time_signature(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust time signature: %r', e)
- def adjust_detune(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_detune(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set audio clip detune.'''
if clip.is_audio_clip:
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
clip.pitch_fine = clip.pitch_fine + factor
else:
try:
@@ -148,14 +167,14 @@ def adjust_detune(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust detune: %r', e)
- def adjust_transpose(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_transpose(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust audio or midi clip transpose, also set audio clip
transpose.
'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
if clip.is_audio_clip:
clip.pitch_coarse = max(-48, min(48, (clip.pitch_coarse + factor)))
elif clip.is_midi_clip:
@@ -185,15 +204,15 @@ def do_note_pitch_adjustment(self, clip, factor):
if edited_notes:
self.write_all_notes(clip, edited_notes, note_data['other_notes'])
- def adjust_gain(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_gain(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set clip gain for Live 9. For settings, range is
0 - 127.
'''
if clip.is_audio_clip:
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
clip.gain = max(0.0, min(1.0, (clip.gain + factor * float(1.0 / 127.0))))
else:
try:
@@ -201,14 +220,14 @@ def adjust_gain(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust gain: %r', e)
- def adjust_start(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_start(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set clip start exclusively for Live 9. In Live 8, same
as adjust_loop_start.
'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
if clip.looping:
clip.start_marker = max(0.0, min(clip.end_marker - factor, (clip.start_marker + factor)))
else:
@@ -222,12 +241,12 @@ def adjust_start(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust start: %r', e)
- def adjust_loop_start(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_loop_start(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set clip loop start if loop is on or clip start otherwise.'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
clip.loop_start = max(0.0, min(clip.loop_end - factor, (clip.loop_start + factor)))
else:
try:
@@ -235,14 +254,14 @@ def adjust_loop_start(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust loop start: %r', e)
- def adjust_end(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_end(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set clip end exclusively for Live 9. In Live 8, same as
adjust_loop_end.
'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
if clip.looping:
clip.end_marker = max((clip.start_marker - factor),
(clip.end_marker + factor))
@@ -258,13 +277,13 @@ def adjust_end(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust end: %r', e)
- def adjust_loop_end(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_loop_end(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust/set clip loop end if loop is on or close end otherwise.
'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
clip.loop_end = max((clip.loop_start - factor), (clip.loop_end + factor))
else:
try:
@@ -272,8 +291,8 @@ def adjust_loop_end(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to adjust loop end: %r', e)
- def adjust_cue_point(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_cue_point(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjust clip's start point and fire (also stores cue point if
not specified). Will not fire xclip itself as this causes a loop.
'''
@@ -281,7 +300,7 @@ def adjust_cue_point(self, clip, track, xclip, ident, args):
if args:
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
args = clip.loop_start + factor
try:
clip.loop_start = float(args)
@@ -297,8 +316,8 @@ def adjust_cue_point(self, clip, track, xclip, ident, args):
if isinstance(xclip, Clip):
xclip.name = '{} {}'.format(xclip.name.strip(), clip.loop_start)
- def adjust_warp_mode(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_warp_mode(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjusts the warp mode of the clip. This cannot be applied if the
warp mode is currently rex (5).
'''
@@ -307,7 +326,7 @@ def adjust_warp_mode(self, clip, track, xclip, ident, args):
if args in WARP_MODES:
clip.warp_mode = WARP_MODES[args]
elif args in ('<', '>'):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
new_mode = clip.warp_mode + factor
if new_mode == 5 and '>' in args:
new_mode = 6
@@ -316,24 +335,24 @@ def adjust_warp_mode(self, clip, track, xclip, ident, args):
if 0 <= new_mode < 7 and new_mode != 5:
clip.warp_mode = new_mode
- def adjust_grid_quantization(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def adjust_grid_quantization(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Adjusts clip grid quantization.'''
args = args.strip()
if args in CLIP_GRID_STATES:
clip.view.grid_quantization = CLIP_GRID_STATES[args]
- def set_triplet_grid(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def set_triplet_grid(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Toggles or turns triplet grid on or off.'''
switch(clip.view, 'grid_is_triplet', args)
- def capture_to_envelope(self, clip, track, xclip, ident, args):
- # type: (Clip, Any, None, None, Text) -> None
+ def capture_to_envelope(self, clip, track, xclip, args):
+ # type: (Clip, Any, None, Text) -> None
self._env_capture.capture(clip, track, args)
- def insert_envelope(self, clip, track, xclip, ident, args):
- # type: (Clip, Any, None, None, Text) -> None
+ def insert_envelope(self, clip, track, xclip, args):
+ # type: (Clip, Any, None, Text) -> None
'''Inserts an envelope for the given parameter into the clip.
This doesn't apply to quantized parameters.
@@ -411,8 +430,8 @@ def _perform_envelope_insertion(self, clip, param, env_type, env_range):
else:
env.insert_step(beat, 1.0, env_range[0])
- def clear_envelope(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def clear_envelope(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Clears the envelope of the specified param or all envelopes
from the given clip.
'''
@@ -423,8 +442,8 @@ def clear_envelope(self, clip, track, xclip, ident, args):
else:
clip.clear_all_envelopes()
- def show_envelope(self, clip, track, xclip, ident, args):
- # type: (Clip, Any, None, None, Text) -> None
+ def show_envelope(self, clip, track, xclip, args):
+ # type: (Clip, Any, None, Text) -> None
'''Shows the clip's envelope view and a particular envelope if
specified. Requires 9.1 or later.
'''
@@ -460,20 +479,20 @@ def _get_envelope_parameter(self, track, args):
param_array = dev_array[1].strip().split()
param = None
if len(param_array) > 1:
- param = self._parent.device_actions.get_banked_parameter(
- dev_array[0], param_array[0], param_array[1])
+ param = self._parent.device_actions.get_bank_param(
+ dev_array[0], param_array[1], param_array[0])
else:
- param = self._parent.device_actions.get_bob_parameter(
+ param = self._parent.device_actions.get_bank_param(
dev_array[0], param_array[0])
return param
- def hide_envelopes(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, None) -> None
+ def hide_envelopes(self, clip, track, xclip, args):
+ # type: (Clip, None, None, None) -> None
'''Hides the clip's envelope view.'''
clip.view.hide_envelope()
- def quantize(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, Text) -> None
+ def quantize(self, clip, track, xclip, args):
+ # type: (Clip, None, None, Text) -> None
'''Quantizes notes or warp markers to the given quantization
value, at the (optional) given strength and with the (optional)
percentage of swing. Can optionally be applied to specific notes
@@ -519,10 +538,10 @@ def quantize(self, clip, track, xclip, ident, args):
clip.quantize_pitch(note, rate_to_apply, strength)
self.song().swing_amount = current_swing
- def duplicate_clip_content(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, None) -> None
- '''Duplicates all the content in a MIDI clip and doubles loop length.
- Will also zoom out to show entire loop if loop is on.
+ def duplicate_clip_content(self, clip, track, xclip, args):
+ # type: (Clip, None, None, None) -> None
+ '''Duplicates all the content in a MIDI clip and doubles loop
+ length. Will also zoom out to show entire loop if loop is on.
'''
if clip.is_midi_clip:
try:
@@ -530,26 +549,26 @@ def duplicate_clip_content(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to duplicate clip content: %r', e)
- def delete_clip(self, clip, track, xclip, ident, args):
- # type: (Clip, None, None, None, None) -> None
+ def delete_clip(self, clip, track, xclip, args):
+ # type: (Clip, None, None, None) -> None
'''Deletes the given clip.'''
clip.canonical_parent.delete_clip()
- def duplicate_clip(self, clip, track, xclip, ident, args):
- # type: (Clip, Track, None, None, None) -> None
- '''Duplicates the given clip. This will overwrite clips if any exist
- in the slots used for duplication.
+ def duplicate_clip(self, clip, track, xclip, args):
+ # type: (Clip, Track, None, None) -> None
+ '''Duplicates the given clip. This will overwrite clips if any
+ exist in the slots used for duplication.
'''
try:
track.duplicate_clip_slot(list(track.clip_slots).index(clip.canonical_parent))
except Exception as e:
log.error('Failed to duplicate clip: %r', e)
- def chop_clip(self, clip, track, xclip, ident, args):
- # type: (Clip, Track, None, None, Text) -> None
- '''Duplicates the clip the number of times specified and sets evenly
- distributed start points across all duplicates. This will overwrite
- clips if any exist in the slots used for duplication.
+ def chop_clip(self, clip, track, xclip, args):
+ # type: (Clip, Track, None, Text) -> None
+ '''Duplicates the clip the number of times specified and sets
+ evenly distributed start points across all duplicates. This will
+ overwrite clips if any exist in the slots used for duplication.
'''
args = args.strip()
num_chops = 8
@@ -572,10 +591,10 @@ def chop_clip(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to chop clip: %r', e)
- def split_clip(self, clip, track, xclip, ident, args):
- # type: (Clip, Track, None, None, Text) -> None
- '''Duplicates the clip and sets each duplicate to have the length
- specified in args. This will overwrite clips if any exist in the
+ def split_clip(self, clip, track, xclip, args):
+ # type: (Clip, Track, None, Text) -> None
+ '''Duplicates the clip and sets each duplicate to have the
+ length specified in args. This will overwrite any clip in the
slots used for duplication.
'''
try:
@@ -603,6 +622,13 @@ def split_clip(self, clip, track, xclip, ident, args):
except Exception as e:
log.error('Failed to split clip: %r', e)
+ def crop(self, clip):
+ '''Crops the clip.
+
+ If looped, removes the region outside of the loop. Otherwise,
+ removes the region outside the start and end markers.'''
+ clip.crop()
+
def get_clip_stats(self, clip):
# type: (Clip) -> Dict[Text, Any]
'''Get real length and end of looping clip.'''
@@ -617,11 +643,36 @@ def get_clip_stats(self, clip):
loop_length = loop_length,
)
+ def is_convertible_to_midi(self, clip):
+ return (clip.is_audio_clip
+ and Conversions.is_convertible_to_midi(self.song(), clip))
+
+ def to_midi(self, clip, track, xclip, args):
+ # type: (Clip) -> None
+ if not self.is_convertible_to_midi(clip):
+ log.info('to_midi not implemented')
+
+ # (Live 10 only) Added CLIP CROP Action for cropping Clips.
+
+ def to_drum_rack(self, clip, track, xclip, args):
+ '''Creates a new track with a Drum Rack with a Simpler on the
+ first pad with the specified audio clip.'''
+ if self.is_convertible_to_midi(clip):
+ Conversions.create_drum_rack_from_audio_clip(self.song(), clip)
+ else:
+ log.warning('Clip not convertible to MIDI')
+
+ def to_simpler(self, clip, track, xclip, args):
+ '''Creates a new MIDI track with a Simpler including the
+ specified audio clip.'''
+ if self.is_convertible_to_midi(clip):
+ Conversions.create_midi_track_with_simpler(self.song(), clip)
+
# TODO: make Mixin together with transport functions
# TODO: check XGlobalActions.do_loop_action and device_looper
# region CLIP LOOP ACTIONS
- def do_clip_loop_action(self, clip, track, xclip, ident, args):
- # type: (Clip, Track, Clip, None, Text) -> None
+ def do_clip_loop_action(self, clip, track, xclip, args):
+ # type: (Clip, Track, Clip, Text) -> None
'''Handle clip loop actions.'''
args = args.strip()
if not args or args.upper() in KEYWORDS:
@@ -629,11 +680,11 @@ def do_clip_loop_action(self, clip, track, xclip, ident, args):
else:
if args.startswith('START'):
self.adjust_loop_start(
- clip, track, xclip, ident, args.replace('START', '', 1).strip()
+ clip, track, xclip, args.replace('START', '', 1).strip()
)
elif args.startswith('END'):
self.adjust_loop_end(
- clip, track, xclip, ident, args.replace('END', '', 1).strip()
+ clip, track, xclip, args.replace('END', '', 1).strip()
)
elif args == 'SHOW':
clip.view.show_loop()
@@ -652,7 +703,7 @@ def do_clip_loop_action(self, clip, track, xclip, ident, args):
new_end = ((clip.loop_end - clip_stats['loop_length'])
+ (clip_stats['loop_length'] * float(args[1:])))
except Exception as e:
- log.error('Failed to do clip action: %r', e)
+ log.error('Failed to do clip loop action: %r', e)
else:
self.do_loop_set(clip, args, clip_stats)
return
@@ -670,7 +721,7 @@ def move_clip_loop_by_factor(self, clip, args, clip_stats):
if args == '<':
factor = -(factor)
if len(args) > 1:
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
new_end = clip.loop_end + factor
new_start = clip.loop_start + factor
if new_start < 0.0:
@@ -680,8 +731,8 @@ def move_clip_loop_by_factor(self, clip, args, clip_stats):
def do_loop_set(self, clip, args, clip_stats):
# type: (Clip, Text, Dict[Text, Any]) -> None
- '''Set loop length and (if clip is playing) position, quantizes to 1/4
- by default or bar if specified.
+ '''Set loop length and (if clip is playing) position, quantizes
+ to 1/4 by default or bar if specified.
'''
try:
qntz = False
@@ -705,8 +756,8 @@ def do_loop_set(self, clip, args, clip_stats):
def set_new_loop_position(self, clip, new_start, new_end, clip_stats):
# type: (Clip, float, float, Dict[Text, Any]) -> None
- '''For use with other clip loop actions, ensures that loop settings
- are within range and applies in correct order.
+ '''For use with other clip loop actions, ensures that loop
+ settings are within range and applies in correct order.
'''
if new_end <= clip_stats['real_end'] and new_start >= 0:
# FIXME: same values
diff --git a/src/clyphx/actions/clip_env_capture.py b/src/clyphx/actions/clip_env_capture.py
index d0b7af1..8574dcc 100644
--- a/src/clyphx/actions/clip_env_capture.py
+++ b/src/clyphx/actions/clip_env_capture.py
@@ -83,13 +83,15 @@ def _capture_nested_devices(self, clip, rack):
if device.can_have_chains and device.chains:
self._capture_nested_devices(clip, device)
- def _insert_envelope(self, clip, param):
+ @staticmethod
+ def _insert_envelope(clip, param):
# type: (Clip, DeviceParameter) -> None
env = clip.automation_envelope(param)
if env:
env.insert_step(clip.loop_start, 0.0, param.value)
- def _get_device_range(self, args, track):
+ @staticmethod
+ def _get_device_range(args, track):
# type: (Text, Track) -> Optional[Tuple[int, int]]
'''Returns range of devices to capture.'''
dev_args = args.replace('MIX', '')
diff --git a/src/clyphx/actions/clip_notes.py b/src/clyphx/actions/clip_notes.py
index 50983f4..a47229e 100644
--- a/src/clyphx/actions/clip_notes.py
+++ b/src/clyphx/actions/clip_notes.py
@@ -16,11 +16,11 @@
from __future__ import absolute_import, unicode_literals
from builtins import object, dict, range
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, NamedTuple, List, Text
import logging
if TYPE_CHECKING:
- from typing import Any, Union, Optional, Text, Dict, List, Tuple
+ from typing import Any, Union, Optional, Dict, Tuple
Note = Tuple[int, float, Any, Any, bool]
NoteActionSignature = Clip, Text, List[Note], List[Note]
@@ -28,6 +28,12 @@
from ..consts import NOTE_NAMES, OCTAVE_NAMES
from ..core.live import Clip, get_random_int
+# from ..core.models import Note
+# from ..core.parse import Pitch
+# NoteData = NamedTuple('NoteData', [('notes_to_edit', List[Note]),
+# ('other_notes', List[Note]),
+# ('args', Text)]) # TODO: Tuple[Text]
+
class ClipNotesMixin(object):
@@ -81,7 +87,8 @@ def get_notes_to_operate_on(self, clip, args=None):
args = new_args,
)
- def get_pos_range(self, clip, string):
+ @staticmethod
+ def get_pos_range(clip, string):
# type: (Clip, Text) -> Tuple[float, float]
'''Get note position or range to operate on.'''
pos_range = (clip.loop_start, clip.loop_end)
@@ -124,7 +131,8 @@ def get_note_range(self, string):
pass
return note_range
- def get_note_range_from_string(self, string):
+ @staticmethod
+ def get_note_range_from_string(string):
# type: (Text) -> Tuple[int, int]
'''Returns a note range (specified in ints) from string.
'''
@@ -138,7 +146,8 @@ def get_note_range_from_string(self, string):
return (start, end)
raise ValueError("Invalid range note: '{}'".format(string))
- def get_note_name_from_string(self, string):
+ @staticmethod
+ def get_note_name_from_string(string):
# type: (Text) -> Text
'''Get the first note name specified in the given string.'''
if len(string) >= 2:
@@ -150,7 +159,8 @@ def get_note_name_from_string(self, string):
return result
raise ValueError("'{}' does not contain a note".format(string))
- def string_to_note(self, string):
+ @staticmethod
+ def string_to_note(string):
# type: (Text) -> Any
'''Get note value from string.'''
base_note = None
@@ -171,7 +181,8 @@ def string_to_note(self, string):
raise ValueError("Invalid note: '{}'".format(string))
- def write_all_notes(self, clip, edited_notes, other_notes):
+ @staticmethod
+ def write_all_notes(clip, edited_notes, other_notes):
# type: (Clip, List[Note], Any) -> None
'''Writes new notes to clip.'''
edited_notes.extend(other_notes)
@@ -199,7 +210,7 @@ def do_note_gate_adjustment(self, clip, args, notes_to_edit, other_notes):
# type: (NoteActionSignature) -> None
'''Adjust note gate.'''
edited_notes = []
- factor = self._parent.get_adjustment_factor(args.split()[1], True)
+ factor = self.get_adjustment_factor(args.split()[1], True)
for n in notes_to_edit:
new_gate = n[2] + (factor * 0.03125)
if n[1] + new_gate > clip.loop_end or new_gate < 0.03125:
@@ -214,7 +225,7 @@ def do_note_nudge_adjustment(self, clip, args, notes_to_edit, other_notes):
# type: (NoteActionSignature) -> None
'''Adjust note position.'''
edited_notes = []
- factor = self._parent.get_adjustment_factor(args.split()[1], True)
+ factor = self.get_adjustment_factor(args.split()[1], True)
for n in notes_to_edit:
new_pos = n[1] + (factor * 0.03125)
if n[2] + new_pos > clip.loop_end or new_pos < 0.0:
@@ -236,7 +247,7 @@ def do_note_velo_adjustment(self, clip, args, notes_to_edit, other_notes):
# FIXME: get_random_int
edited_notes.append((n[0], n[1], n[2], get_random_int(64, 64), n[4]))
elif args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
new_velo = n[3] + factor
if 0 <= new_velo < 128:
edited_notes.append((n[0], n[1], n[2], new_velo, n[4]))
diff --git a/src/clyphx/actions/consts.py b/src/clyphx/actions/consts.py
index 591e965..c9026eb 100644
--- a/src/clyphx/actions/consts.py
+++ b/src/clyphx/actions/consts.py
@@ -87,6 +87,12 @@
MAKE_DEV_DOC = XGlobalActions.make_instant_mapping_docs,
) # type: Dict[Text, Callable]
+SCENE_ACIONS = dict(
+ ADD = XGlobalActions.create_scene,
+ DEL = XGlobalActions.delete_scene,
+ DUPE = XGlobalActions.duplicate_scene,
+)
+
TRACK_ACTIONS = dict(
ARM = XTrackActions.set_arm,
MUTE = XTrackActions.set_mute,
@@ -137,12 +143,16 @@
DEL = XClipActions.delete_clip,
DUPE = XClipActions.duplicate_clip,
CHOP = XClipActions.chop_clip,
+ CROP = XClipActions.crop,
SPLIT = XClipActions.split_clip,
WARPMODE = XClipActions.adjust_warp_mode,
LOOP = XClipActions.do_clip_loop_action,
SIG = XClipActions.adjust_time_signature,
WARP = XClipActions.set_warp,
NAME = XClipActions.set_clip_name,
+ # TOMIDI = XClipActions.to_midi,
+ TODR = XClipActions.to_drum_rack,
+ TOSIMP = XClipActions.to_simpler,
) # type: Dict[Text, Callable]
CLIP_NOTE_ACTIONS_CMD = dict([
diff --git a/src/clyphx/actions/control_surface.py b/src/clyphx/actions/control_surface.py
index 2a7f4a9..fdcf5be 100644
--- a/src/clyphx/actions/control_surface.py
+++ b/src/clyphx/actions/control_surface.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with ClyphX. If not, see .
-from __future__ import with_statement, absolute_import, unicode_literals
+from __future__ import absolute_import, unicode_literals
from builtins import super, dict, range
from functools import partial
from typing import TYPE_CHECKING
@@ -152,11 +152,11 @@ def _handle_push2_init(self, index):
def dispatchers(self):
return dict(
SURFACE = self.dispatch_cs_action,
- CS = self.dispatch_cs_action,
+ CS = self.dispatch_cs_action,
ARSENAL = self.dispatch_arsenal_action,
- PUSH = self.dispatch_push_action,
- PTX = self.dispatch_pxt_action,
- MTX = self.dispatch_mxt_action,
+ PUSH = self.dispatch_push_action,
+ PTX = self.dispatch_pxt_action,
+ MTX = self.dispatch_mxt_action,
)
# TODO: normalize dispatching
@@ -176,37 +176,25 @@ def dispatch_action(self, cmd):
def dispatch_cs_action(self, script, xclip, ident, args):
# type: (Any, Clip, Text, Text) -> None
'''Dispatch appropriate control surface actions.'''
- if 'METRO ' in args and 'metro' in self._scripts[script]:
- self.handle_visual_metro(self._scripts[script], args)
- elif 'RINGLINK ' in args and self._scripts[script]['session']:
- self.handle_ring_link(
- self._scripts[script]['session'], script, args[9:]
- )
- elif 'RING ' in args and self._scripts[script]['session']:
- self.handle_session_offset(script,
- self._scripts[script]['session'],
- args[5:])
- elif ('COLORS ' in args and
- self._scripts[script]['session'] and
- self._scripts[script]['color']):
- self.handle_session_colors(self._scripts[script]['session'],
- self._scripts[script]['color'],
- args[7:])
- elif 'DEV LOCK' in args and self._scripts[script]['device']:
- self._scripts[script]['device'].canonical_parent.toggle_lock()
- elif 'BANK ' in args and self._scripts[script]['mixer']:
+ _script = self._scripts[script]
+
+ if 'METRO ' in args and 'metro' in _script:
+ self.handle_visual_metro(_script, args)
+ elif 'RINGLINK ' in args and _script['session']:
+ self.handle_ring_link(_script['session'], script, args[9:])
+ elif 'RING ' in args and _script['session']:
+ self.handle_session_offset(script, _script['session'], args[5:])
+ elif ('COLORS ' in args and _script['session'] and _script['color']):
+ self.handle_session_colors(_script['session'], _script['color'], args[7:])
+ elif 'DEV LOCK' in args and _script['device']:
+ _script['device'].canonical_parent.toggle_lock()
+ elif 'BANK ' in args and _script['mixer']:
self.handle_track_bank(
- script, xclip, ident, self._scripts[script]['mixer'],
- self._scripts[script]['session'], args[5:]
- )
+ script, xclip, ident, _script['mixer'], _script['session'], args[5:])
elif 'RPT' in args:
- self.handle_note_repeat(
- self._scripts[script]['script'], script, args
- )
- elif self._scripts[script]['mixer'] and '/' in args[:4]:
- self.handle_track_action(
- script, self._scripts[script]['mixer'], xclip, ident, args
- )
+ self.handle_note_repeat(_script['script'], script, args)
+ elif _script['mixer'] and '/' in args[:4]:
+ self.handle_track_action(script, _script['mixer'], xclip, ident, args)
def dispatch_push_action(self, track, xclip, ident, action, args):
# type: (Track, Clip, Text, None, Text) -> None
@@ -307,9 +295,10 @@ def handle_track_action(self, script_key, mixer, xclip, ident, args):
if (0 <= track_start and track_start < track_end and
track_end < len(mixer._channel_strips) + 1):
track_list = []
- if self._scripts[script_key]['name'] == 'PUSH':
- offset, _ = self._push_actions.get_session_offsets(self._scripts[script_key]['session'])
- tracks_to_use = self._scripts[script_key]['session'].tracks_to_use()
+ _script = self._scripts[script_key]
+ if _script['name'] == 'PUSH':
+ offset, _ = self._push_actions.get_session_offsets(_script['session'])
+ tracks_to_use = _script['session'].tracks_to_use()
else:
offset = mixer._track_offset
tracks_to_use = mixer.tracks_to_use()
@@ -390,15 +379,14 @@ def _parse_ring_spec(self, spec_id, arg_string, default_index, list_to_search):
specification that was parsed.
'''
index = default_index
- arg_array = arg_string.split()
- for a in arg_array:
+ for a in arg_string.split():
if a.startswith(spec_id):
if a[1].isdigit():
index = int(a.strip(spec_id)) - 1
arg_string = arg_string.replace(a, '', 1).strip()
break
elif a[1] in ('<', '>'):
- index += self._parent.get_adjustment_factor(a.strip(spec_id))
+ index += self.get_adjustment_factor(a.strip(spec_id))
arg_string = arg_string.replace(a, '', 1).strip()
break
elif a[1] == '"':
@@ -419,9 +407,10 @@ def handle_ring_link(self, session, script_index, args):
'''Handles linking/unliking session offsets to the selected
track or scene with centering if specified.
'''
- self._scripts[script_index]['track_link'] = args == 'T' or 'T ' in args or ' T' in args
- self._scripts[script_index]['scene_link'] = 'S' in args
- self._scripts[script_index]['centered_link'] = 'CENTER' in args
+ script = self._scripts[script_index]
+ script['track_link'] = args == 'T' or 'T ' in args or ' T' in args
+ script['scene_link'] = 'S' in args
+ script['centered_link'] = 'CENTER' in args
def handle_session_colors(self, session, colors, args):
# type: (Any, Any, Text) -> None
@@ -449,21 +438,20 @@ def handle_visual_metro(self, script, args):
This is a specialized version for L9 that uses component guard
to avoid dependency issues.
'''
- if 'ON' in args and not script['metro']['component']:
+ metro = script['metro']
+ if 'ON' in args and not metro['component']:
with self._parent.component_guard():
- m = VisualMetro(self._parent,
- script['metro']['controls'],
- script['metro']['override'])
- script['metro']['component'] = m
- elif 'OFF' in args and script['metro']['component']:
- script['metro']['component'].disconnect()
- script['metro']['component'] = None
+ m = VisualMetro(self._parent, metro['controls'], metro['override'])
+ metro['component'] = m
+ elif 'OFF' in args and metro['component']:
+ metro['component'].disconnect()
+ metro['component'] = None
def on_selected_track_changed(self):
'''Moves the track offset of all track linked surfaces to the
selected track with centering if specified.
'''
- trk = self.song().view.selected_track
+ trk = self.sel_track
if trk in self.song().tracks:
trk_id = list(self.song().visible_tracks).index(trk)
for k, v in self._scripts.items():
@@ -496,7 +484,7 @@ def on_selected_scene_changed(self):
'''Moves the scene offset of all scene linked surfaces to the
selected scene with centering if specified.
'''
- scn_id = list(self.song().scenes).index(self.song().view.selected_scene)
+ scn_id = self.sel_scene
for k, v in self._scripts.items():
if v['scene_link']:
new_scn_id = scn_id
diff --git a/src/clyphx/actions/device.py b/src/clyphx/actions/device.py
index 9127a3b..27c2c4b 100644
--- a/src/clyphx/actions/device.py
+++ b/src/clyphx/actions/device.py
@@ -5,7 +5,7 @@
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
-#
+##
# ClyphX is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
@@ -15,22 +15,20 @@
# along with ClyphX. If not, see .
from __future__ import absolute_import, unicode_literals
-from builtins import super, dict, range
+from builtins import super, dict, range, list
from typing import TYPE_CHECKING
import logging
if TYPE_CHECKING:
- from typing import Any, Sequence, Optional, Text, List, Dict
+ from typing import Any, Sequence, Optional, Text, List, Dict, Callable
from ..core.live import Device, DeviceParameter, Track
from ..core.legacy import _DispatchCommand, _SingleDispatch
-from _Generic.Devices import (
- number_of_parameter_banks, get_parameter_by_name,
- DEVICE_DICT, DEVICE_BOB_DICT,
-)
+from _Generic.Devices import get_parameter_by_name
+from ..core.exceptions import InvalidAction, InvalidParam
from ..core.xcomponent import XComponent
from ..core.live import Clip, get_random_int
-from ..consts import switch
+from ..consts import switch, DEVICE_BANKS
from .device_looper import LooperMixin
log = logging.getLogger(__name__)
@@ -60,15 +58,19 @@ def dispatch_device_actions(self, cmd):
device_action = self._parent.get_device_to_operate_on(*_args)
if device_action[0]:
try:
+ # TODO: substitute devargs with splitargs
device_args = device_action[1]
+ split_args = [x.strip().upper() for x in device_action[1].split() if x.strip()]
except IndexError:
device_args = None
+ split_args = None
- if device_args and device_args.split()[0] in DEVICE_ACTIONS:
- func = DEVICE_ACTIONS[device_args.split()[0]]
+ if split_args and split_args[0] in DEVICE_ACTIONS:
+ func = DEVICE_ACTIONS[split_args[0]]
+ # TODO: pass split_args instead of device_args
func(self, device_action[0], scmd.track, scmd.xclip, device_args)
- elif device_args and 'CHAIN' in device_args:
- self.dispatch_chain_action(device_action[0], device_args)
+ elif split_args and 'CHAIN' in device_args:
+ self.dispatch_chain_action(device_action[0], split_args)
elif cmd.action_name.startswith('DEV'):
self.set_device_on_off(device_action[0], device_args)
@@ -76,8 +78,8 @@ def select_device(self, device, track, xclip, args):
# type: (Device, Track, None, None, None) -> None
'''Select device and bring it and the track it's on into view.
'''
- if self.song().view.selected_track != track:
- self.song().view.selected_track = track
+ if self.sel_track != track:
+ self.sel_track = track
self.application().view.show_view('Detail')
self.application().view.show_view('Detail/DeviceChain')
self.song().view.select_device(device)
@@ -95,7 +97,7 @@ def set_all_params(self, device, track, xclip, args):
param_values = args.split()
if len(param_values) == 8:
for i in range(8):
- self._parent.do_parameter_adjustment(
+ self.adjust_param(
device.parameters[i + 1],
param_values[i].strip()
)
@@ -128,9 +130,9 @@ def adjust_bob_param(self, device, track, xclip, args):
param = None
name_split = args.split()
if len(name_split) > 1:
- param = self.get_bob_parameter(device, name_split[0])
+ param = self.get_bank_param(device, name_split[0])
if param and param.is_enabled:
- self._parent.do_parameter_adjustment(param, name_split[-1])
+ self.adjust_param(param, name_split[-1])
def adjust_banked_param(self, device, track, xclip, args):
# type: (Device, None, None, Text) -> None
@@ -138,9 +140,9 @@ def adjust_banked_param(self, device, track, xclip, args):
param = None
name_split = args.split()
if len(name_split) > 2:
- param = self.get_banked_parameter(device, name_split[0], name_split[1])
+ param = self.get_bank_param(device, name_split[1], name_split[0])
if param and param.is_enabled:
- self._parent.do_parameter_adjustment(param, name_split[-1])
+ self.adjust_param(param, name_split[-1])
def adjust_chain_selector(self, device, track, xclip, args):
# type: (Device, None, None, Text) -> None
@@ -148,31 +150,29 @@ def adjust_chain_selector(self, device, track, xclip, args):
param = self.get_chain_selector(device)
name_split = args.split()
if param and param.is_enabled and len(name_split) > 1:
- self._parent.do_parameter_adjustment(param, name_split[-1])
+ self.adjust_param(param, name_split[-1])
def randomize_params(self, device, track, xclip, args):
# type: (Device, None, None, None) -> None
'''Randomize device parameters.'''
- name = device.name.upper()
- if not name.startswith(
- ('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN',
- 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')
- ):
- for p in device.parameters:
- if p and p.is_enabled and not p.is_quantized and p.name != 'Chain Selector':
- p.value = (((p.max - p.min) / 127) * get_random_int(0, 128)) + p.min
+ func = lambda p: (((p.max - p.min) / 127) * get_random_int(0, 128)) + p.min
+ self._rnr_params(device, func)
def reset_params(self, device, track, xclip, args):
# type: (Device, None, None, None) -> None
'''Reset device parameters.'''
- name = device.name.upper()
- if not name.startswith(
- ('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN',
- 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')
- ):
+ func = lambda p: p.default_value
+ self._rnr_params(device, func)
+
+ @staticmethod
+ def _rnr_params(device, func):
+ # type: (Device, Callable) -> None
+ from ..macrobat.consts import RNR_EXCLUDED
+
+ if not device.name.upper().startswith(RNR_EXCLUDED):
for p in device.parameters:
if p and p.is_enabled and not p.is_quantized and p.name != 'Chain Selector':
- p.value = p.default_value
+ p.value = func(p)
def set_device_on_off(self, device, value=None):
# type: (Device, Optional[Text]) -> None
@@ -182,7 +182,8 @@ def set_device_on_off(self, device, value=None):
switch(param, 'value', value)
# TODO: break?
- def get_chain_selector(self, device):
+ @staticmethod
+ def get_chain_selector(device):
# type: (Device) -> Optional[DeviceParameter]
'''Get rack chain selector param.'''
if device.class_name.endswith('GroupDevice'):
@@ -191,74 +192,61 @@ def get_chain_selector(self, device):
return parameter
return None
- def get_bob_parameter(self, device, param_string):
- # type: (Device, Text) -> Optional[DeviceParameter]
- '''Get best-of-bank parameter 1-8 for Live's devices.
-
- The param string should be composed of 'P' followed by the param
- index (like P5).
+ @staticmethod
+ def get_bank_param(device, param, bank='B0'):
+ # type: (Device, str, Optional[str]) -> DeviceParameter
+ '''Get bank/parameter for Live's devices.
+
+ Args:
+ param: should be composed of 'P' followed by the param
+ index (1-8), e.g. 'P5'.
+ bank: if provided should be composed of 'B' followed by the
+ bank index (1-8), e.g. 'B2'. Otherwise returns a BoB
+ (best-of-bank) param.
'''
try:
- param_bank = DEVICE_BOB_DICT[device.class_name][0]
- param_num = int(param_string[1]) - 1
- if 0 <= param_num < 8:
- parameter = get_parameter_by_name(device, param_bank[param_num])
- if parameter:
- return parameter
- except:
- return None
-
- def get_banked_parameter(self, device, bank_string, param_string):
- # type: (Device, Text, Text) -> Optional[DeviceParameter]
- '''Get bank 1-8/parameter 1-8 for Live's devices.
+ _param = int(param[1]) - 1
+ _bank = int(bank[1])
+ name = DEVICE_BANKS[device.class_name][_bank][_param]
+ # TODO: join with dev_doc
+ return get_parameter_by_name(device, name)
+ except Exception as e:
+ log.error('Failed to get banked param (%s/%s): %r',
+ bank if _bank else 'BoB', param, e)
- :param bank_string: should be composed of 'B' followed by the
- bank index (like B2)
- :param param_string: should be composed of 'P' followed by the
- param index (like P5).
- '''
- try:
- device_bank = DEVICE_DICT[device.class_name]
- bank_num = int(bank_string[1]) - 1
- param_num = int(param_string[1]) - 1
- if 0 <= param_num < 8 and 0 <= bank_num < 8 <= number_of_parameter_banks(device):
- param_bank = device_bank[bank_num]
- parameter = get_parameter_by_name(device, param_bank[param_num])
- if parameter:
- return parameter
- except:
- pass
- return None
# region CHAIN ACTIONS
def dispatch_chain_action(self, device, args):
- # type: (Device, Text) -> None
+ # type: (Device, List[Text]) -> None
'''Handle actions related to device chains.'''
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.')
- arg_list = [x.strip() for x in args.upper().split()]
try:
- chain_num, action, value = arg_list[0:3]
+ chain_num, action, value = args[0:3]
chain = device.chains[int(chain_num.replace('CHAIN', '')) - 1]
if action not in {'MUTE', 'SOLO', 'VOL', 'PAN'}:
- raise AttributeError('Invalid device chain action: {}'.format(action))
+ raise InvalidAction('Invalid device chain action: {}'.format(action))
elif action in {'VOL', 'PAN'} and device.class_name.startswith('Midi'):
- raise AttributeError('Invalid MIDI device chain action: {}'.format(action))
+ raise InvalidAction('Invalid MIDI device chain action: {}'.format(action))
except Exception as e:
- log.error("Failed to parse chain action args '%s': %r", arg_list, e)
+ log.error("Failed to parse chain action args '%s': %r", args, e)
return
chain = device.view.selected_chain
if chain:
- func, param = dict(
- MUTE = (switch, chain.mute),
- SOLO = (switch, chain.solo),
- VOL = (self._parent.do_parameter_adjustment, chain.mixer_device.volume),
- PAN = (self._parent.do_parameter_adjustment, chain.mixer_device.panning),
+ 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(param, value)
+ func(obj, param, value)
+ # FIXME:
+ def _adjust_param(self, obj, param, value):
+ self.adjust_param(getattr(obj, param), value)
# endregion
diff --git a/src/clyphx/actions/device_looper.py b/src/clyphx/actions/device_looper.py
index 85e077d..ee2c4cc 100644
--- a/src/clyphx/actions/device_looper.py
+++ b/src/clyphx/actions/device_looper.py
@@ -2,6 +2,7 @@
from builtins import object
from ..consts import LOOPER_STATES, switch
+from ..core.exceptions import InvalidParam
# TODO: turn into a component?
@@ -61,4 +62,4 @@ def set_looper_state(self, value=None):
try:
self.get_param('State').value = LOOPER_STATES[value.upper()]
except KeyError:
- raise ValueError("'{}' is not a looper state".format(value))
+ raise InvalidParam("'{}' is not a looper state".format(value))
diff --git a/src/clyphx/actions/dr.py b/src/clyphx/actions/dr.py
index 4d71899..4c79ee8 100644
--- a/src/clyphx/actions/dr.py
+++ b/src/clyphx/actions/dr.py
@@ -62,7 +62,7 @@ def scroll_selector(self, dr, args):
'''Scroll Drum Rack selector up/down.'''
args = args.replace('SCROLL', '').strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
pos = dr.view.drum_pads_scroll_position
if factor > 0:
if pos < MAX_SCROLL_POS - factor:
@@ -125,7 +125,7 @@ def _adjust_pad_volume(self, pads, action_arg):
for pad in pads:
if pad.chains:
param = pad.chains[0].mixer_device.volume
- self._parent.do_parameter_adjustment(param, action_arg)
+ self.adjust_param(param, action_arg)
def _adjust_pad_pan(self, pads, action_arg):
# type: (Sequence[Any], Text) -> None
@@ -133,7 +133,7 @@ def _adjust_pad_pan(self, pads, action_arg):
for pad in pads:
if pad.chains:
param = pad.chains[0].mixer_device.panning
- self._parent.do_parameter_adjustment(param, action_arg)
+ self.adjust_param(param, action_arg)
def _adjust_pad_send(self, pads, action_arg, send):
# type: (Sequence[Any], Text, Text) -> None
@@ -142,11 +142,12 @@ def _adjust_pad_send(self, pads, action_arg, send):
for pad in pads:
if pad.chains:
param = pad.chains[0].mixer_device.sends[ord(send) - 65]
- self._parent.do_parameter_adjustment(param, action_arg)
+ self.adjust_param(param, action_arg)
except:
pass
- def _get_pads_to_operate_on(self, dr, pads):
+ @staticmethod
+ def _get_pads_to_operate_on(dr, pads):
# type: (Any, Text) -> Sequence[Any]
'''Get the Drum Rack pad or pads to operate on.'''
pads_to_operate_on = [dr.view.selected_drum_pad]
diff --git a/src/clyphx/actions/global_.py b/src/clyphx/actions/global_.py
index 8a6551d..8489257 100644
--- a/src/clyphx/actions/global_.py
+++ b/src/clyphx/actions/global_.py
@@ -35,11 +35,12 @@
from ..consts import (AUDIO_DEVS, MIDI_DEVS, INS_DEVS,
GQ_STATES, REPEAT_STATES, RQ_STATES,
MIDI_STATUS)
+from .scene import SceneMixin
log = logging.getLogger(__name__)
-class XGlobalActions(XComponent):
+class XGlobalActions(XComponent, SceneMixin):
'''Global actions.
'''
__module__ = __name__
@@ -60,7 +61,7 @@ def __init__(self, parent):
self._last_gqntz = int(self.song().clip_trigger_quantization)
if self.song().midi_recording_quantization != 0:
self._last_rqntz = int(self.song().midi_recording_quantization)
- self._last_scene_index = list(self.song().scenes).index(self.song().view.selected_scene)
+ self._last_scene_index = self.sel_scene
self._scenes_to_monitor = list() # type: List[Scene]
self.setup_scene_listeners()
@@ -76,24 +77,23 @@ def dispatch_action(self, cmd):
# type: (_SingleDispatch) -> None
from .consts import GLOBAL_ACTIONS
- action = GLOBAL_ACTIONS[cmd.action_name]
- action(self, cmd.track, cmd.xclip, cmd.ident, cmd.args)
-
- def on_scene_triggered(self, index):
- self._last_scene_index = index
-
- def on_scene_list_changed(self):
- self.setup_scene_listeners()
+ 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, ident, args):
- # type: (None, Clip, None, Text) -> None
+ def make_instant_mapping_docs(self, track, xclip, args):
+ # type: (None, Clip, Text) -> 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, ident, args):
- # type: (None, None, None, Text) -> None
+ 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:
@@ -129,8 +129,8 @@ def send_midi_message(self, track, xclip, ident, args):
except:
pass
- def do_variable_assignment(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def do_variable_assignment(self, track, xclip, args):
+ # 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.
'''
@@ -147,8 +147,8 @@ def do_variable_assignment(self, track, xclip, ident, args):
# region TRACKS
- def create_audio_track(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def create_audio_track(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Creates audio track at end of track list or at the specified
index.
'''
@@ -162,7 +162,7 @@ def create_audio_track(self, track, xclip, ident, value=None):
else:
self.song().create_audio_track(-1)
- def create_midi_track(self, track, xclip, ident, value=None):
+ def create_midi_track(self, track, xclip, value=None):
# type: (None, None, None, Optional[Text]) -> None
'''Creates MIDI track at end of track list or at the specified
index.
@@ -177,20 +177,20 @@ def create_midi_track(self, track, xclip, ident, value=None):
else:
self.song().create_midi_track(-1)
- def create_return_track(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def create_return_track(self, track, xclip, value=None):
+ # 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, ident, value=None):
- # type: (None, None, None, None) -> None
+ def insert_and_configure_audio_track(self, track, xclip, value=None):
+ # 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, ident, value=None):
- # type: (None, None, None, None) -> None
+ def insert_and_configure_midi_track(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Inserts a midi track next to the selected track routed from
the selected track and armed.
'''
@@ -202,7 +202,7 @@ def _insert_and_configure_track(self, is_midi=False):
will only work if the selected track has the appropriate output/
input for the insertion.
'''
- sel_track = self.song().view.selected_track
+ sel_track = self.sel_track
if is_midi and not sel_track.has_midi_input:
return
if not is_midi and not sel_track.has_audio_output:
@@ -222,8 +222,8 @@ def _insert_and_configure_track(self, is_midi=False):
# endregion
- def swap_device_preset(self, track, xclip, ident, args):
- # type: (Track, None, None, Text) -> None
+ 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
forwards and back through presets.
@@ -254,7 +254,7 @@ def _handle_swapping(self, device, browser_item, args):
# type: (Device, Any, Text) -> None
dev_items = self._create_device_items(browser_item, [])
if args in ('<', '>'):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
index = self._get_current_preset_index(device, dev_items)
new_index = index + factor
if new_index > len(dev_items) - 1:
@@ -301,8 +301,8 @@ def _create_device_items(self, device, item_array):
item_array.append(item)
return item_array
- def load_device(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ 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!
@@ -326,8 +326,8 @@ def load_device(self, track, xclip, ident, args):
self.application().browser.load_item(dev)
break
- def load_m4l(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ 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.
'''
@@ -346,13 +346,13 @@ def load_m4l(self, track, xclip, ident, args):
self.application().browser.load_item(found_dev)
break
- def set_session_record(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_session_record(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off session record.'''
switch(self.song(), 'session_record', value)
- def trigger_session_record(self, track, xclip, ident, value=None):
- # type: (None, Clip, None, Optional[Text]) -> None
+ def trigger_session_record(self, track, xclip, value=None):
+ # type: (None, Clip, Optional[Text]) -> None
'''Triggers session record in all armed tracks for the specified
fixed length.
'''
@@ -383,13 +383,13 @@ def _track_has_empty_slot(self, track, start):
return True
return False
- def set_session_automation_record(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_session_automation_record(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off session automation record.'''
switch(self.song(), 'session_automation_record', value)
- def retrigger_recording_clips(self, track, xclip, ident, value=None):
- # type: (Track, None, None, None) -> None
+ def retrigger_recording_clips(self, track, xclip, value=None):
+ # type: (Track, None, None) -> None
'''Retriggers all clips that are currently recording.'''
for track in self.song().tracks:
if track.playing_slot_index >= 0:
@@ -397,92 +397,92 @@ def retrigger_recording_clips(self, track, xclip, ident, value=None):
if slot.has_clip and slot.clip.is_recording:
slot.fire()
- def set_back_to_arrange(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_back_to_arrange(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Triggers back to arrange button.'''
self.song().back_to_arranger = 0
- def set_overdub(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_overdub(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off overdub.'''
switch(self.song(), 'overdub', value)
- def set_metronome(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_metronome(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off metronome.'''
switch(self.song(), 'metronome', value)
- def set_record(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_record(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off record.'''
switch(self.song(), 'record_mode', value)
- def set_punch_in(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_punch_in(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off punch in.'''
switch(self.song(), 'punch_in', value)
- def set_punch_out(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_punch_out(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Toggles or turns on/off punch out.'''
switch(self.song(), 'punch_out', value)
- def restart_transport(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def restart_transport(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Restarts transport to 0.0'''
self.song().current_song_time = 0
- def set_stop_transport(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_stop_transport(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Toggles transport.'''
self.song().is_playing = not self.song().is_playing
- def set_continue_playback(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_continue_playback(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Continue playback from stop point.'''
self.song().continue_playing()
- def set_stop_all(self, track, xclip, ident, value=None):
- # type: (None, None, None, Optional[Text]) -> None
+ def set_stop_all(self, track, xclip, value=None):
+ # type: (None, None, Optional[Text]) -> None
'''Stop all clips w/no quantization option for Live 9.'''
self.song().stop_all_clips((value or '').strip() != 'NQ')
- def set_tap_tempo(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_tap_tempo(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Tap tempo.'''
self.song().tap_tempo()
- def set_undo(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_undo(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Triggers Live's undo.'''
if self.song().can_undo:
self.song().undo()
- def set_redo(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def set_redo(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Triggers Live's redo.'''
if self.song().can_redo:
self.song().redo()
# region NAVIGATION
- def move_up(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_up(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Scroll up.'''
self._move_nav(0)
- def move_down(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_down(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Scroll down.'''
self._move_nav(1)
- def move_left(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_left(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Scroll left.'''
self._move_nav(2)
- def move_right(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_right(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Scroll right.'''
self._move_nav(3)
@@ -492,19 +492,19 @@ def _move_nav(self, direction):
Application.View.NavDirection(direction), '', False
)
- def move_to_first_device(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_to_first_device(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Move to the first device on the track and scroll the view.'''
self.focus_devices()
- self.song().view.selected_track.view.select_instrument()
+ self.sel_track.view.select_instrument()
- def move_to_last_device(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_to_last_device(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Move to the last device on the track and scroll the view.'''
self.focus_devices()
- if self.song().view.selected_track.devices:
+ if self.sel_track.devices:
self.song().view.select_device(
- self.song().view.selected_track.devices[len(self.song().view.selected_track.devices) - 1]
+ self.sel_track.devices[len(self.sel_track.devices) - 1]
)
self.application().view.scroll_view(
Application.View.NavDirection(3), 'Detail/DeviceChain', False
@@ -513,16 +513,16 @@ def move_to_last_device(self, track, xclip, ident, value=None):
Application.View.NavDirection(2), 'Detail/DeviceChain', False
)
- def move_to_prev_device(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_to_prev_device(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Move to the previous device on the track.'''
self.focus_devices()
self.application().view.scroll_view(
Application.View.NavDirection(2), 'Detail/DeviceChain', False
)
- def move_to_next_device(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def move_to_next_device(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Move to the next device on the track.'''
self.focus_devices()
self.application().view.scroll_view(
@@ -534,28 +534,28 @@ def focus_devices(self):
self.application().view.show_view('Detail')
self.application().view.show_view('Detail/DeviceChain')
- def show_clip_view(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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')
- def show_track_view(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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')
- def show_detail_view(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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')
else:
self.application().view.show_view('Detail')
- def toggle_browser(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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')
@@ -564,8 +564,8 @@ def toggle_browser(self, track, xclip, ident, value=None):
self.application().view.show_view('Browser')
self.application().view.focus_view('Browser')
- def toggle_detail_view(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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'):
@@ -573,16 +573,16 @@ def toggle_detail_view(self, track, xclip, ident, value=None):
else:
self.application().view.show_view('Detail/Clip')
- def toggle_main_view(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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')
else:
self.application().view.show_view('Session')
- def focus_browser(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ def focus_browser(self, track, xclip, value=None):
+ # type: (None, None, None) -> None
'''Move the focus to the browser, show browser first if
necessary.
'''
@@ -590,8 +590,8 @@ def focus_browser(self, track, xclip, ident, value=None):
self.application().view.show_view('Browser')
self.application().view.focus_view('Browser')
- def focus_detail(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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.
'''
@@ -599,13 +599,13 @@ def focus_detail(self, track, xclip, ident, value=None):
self.application().view.show_view('Detail')
self.application().view.focus_view('Detail')
- def focus_main(self, track, xclip, ident, value=None):
- # type: (None, None, None, None) -> None
+ 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('')
- def adjust_horizontal_zoom(self, track, xclip, ident, value):
- # type: (None, None, None, Text) -> None
+ def adjust_horizontal_zoom(self, track, xclip, value):
+ # type: (None, None, Text) -> None
'''Horizontally zoom in in Arrange the number of times specified
in value. This can accept ALL, but doesn't have any bearing.
'''
@@ -623,8 +623,8 @@ def adjust_horizontal_zoom(self, track, xclip, ident, value):
Application.View.NavDirection(direct), '', zoom_all
)
- def adjust_vertical_zoom(self, track, xclip, ident, value):
- # type: (None, None, None, Text) -> None
+ def adjust_vertical_zoom(self, track, xclip, value):
+ # type: (None, None, Text) -> None
'''Vertically zoom in on the selected track in Arrange the
number of times specified in value. This can accept ALL for
zooming all tracks.
@@ -641,14 +641,14 @@ def adjust_vertical_zoom(self, track, xclip, ident, value):
# endregion
- def adjust_tempo(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_tempo(self, track, xclip, args):
+ # 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._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
self.song().tempo = max(20, min(999, (self.song().tempo + factor)))
elif args.startswith('*'):
try:
@@ -699,12 +699,12 @@ def apply_tempo_ramp(self, arg=None):
else:
self.song().tempo += self._tempo_ramp_settings[1]
- def adjust_groove(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_groove(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Adjust/set global groove.'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
self.song().groove_amount = max(0.0, min(1.3125, self.song().groove_amount + factor * float(1.3125 / 131.0)))
else:
try:
@@ -712,8 +712,8 @@ def adjust_groove(self, track, xclip, ident, args):
except:
pass
- def set_note_repeat(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def set_note_repeat(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Set/toggle note repeat.'''
args = args.strip()
if args == 'OFF':
@@ -727,12 +727,12 @@ def set_note_repeat(self, track, xclip, ident, args):
self._repeat_enabled = not self._repeat_enabled
self._parent._c_instance.note_repeat.enabled = self._repeat_enabled
- def adjust_swing(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_swing(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Adjust swing amount for use with note repeat.'''
args = args.strip()
if args.startswith(('<', '>')):
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
self.song().swing_amount = max(0.0, min(1.0, (self.song().swing_amount + factor * 0.01)))
else:
try:
@@ -740,14 +740,14 @@ def adjust_swing(self, track, xclip, ident, args):
except:
pass
- def adjust_global_quantize(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_global_quantize(self, track, xclip, args):
+ # 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._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
new_gq = self.song().clip_trigger_quantization + factor
if 0 <= new_gq < 14:
self.song().clip_trigger_quantization = new_gq
@@ -757,14 +757,14 @@ def adjust_global_quantize(self, track, xclip, ident, args):
else:
self.song().clip_trigger_quantization = self._last_gqntz
- def adjust_record_quantize(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_record_quantize(self, track, xclip, args):
+ # 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._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
new_rq = self.song().midi_recording_quantization + factor
if 0 <= new_rq < 9:
self.song().midi_recording_quantization = new_rq
@@ -774,8 +774,8 @@ def adjust_record_quantize(self, track, xclip, ident, args):
else:
self.song().midi_recording_quantization = self._last_rqntz
- def adjust_time_signature(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def adjust_time_signature(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Adjust global time signature.'''
if '/' in args:
try:
@@ -785,37 +785,37 @@ def adjust_time_signature(self, track, xclip, ident, args):
except:
pass
- def set_jump_all(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def set_jump_all(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Jump arrange position forward/backward.'''
try:
self.song().jump_by(float(args))
except:
pass
- def set_unarm_all(self, track, xclip, ident, args):
- # type: (None, None, None, None) -> None
+ 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, ident, args):
- # type: (None, None, None, None) -> None
+ 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, ident, args):
- # type: (None, None, None, None) -> None
+ 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
- def set_fold_all(self, track, xclip, ident, value):
- # type: (None, None, None, None) -> None
+ def set_fold_all(self, track, xclip, value):
+ # type: (None, None, None) -> None
'''Toggle or turn/on fold for all track.'''
state_to_set = None
for t in self.song().tracks:
@@ -824,20 +824,20 @@ def set_fold_all(self, track, xclip, ident, value):
state_to_set = not t.fold_state
switch(t, 'fold_state', value, state_to_set)
- def set_locator(self, track, xclip, ident, args):
- # type: (None, None, None, None) -> None
+ 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, ident, args):
- # type: (None, None, None, Text) -> None
+ def do_locator_loop_action(self, track, xclip, args):
+ # 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, ident, args, True)
+ self.do_locator_action(track, xclip, args, True)
- def do_locator_action(self, track, xclip, ident, args, move_loop_too=False):
- # type: (None, None, None, Text, bool) -> None
+ def do_locator_action(self, track, xclip, args, 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.
'''
@@ -857,8 +857,8 @@ def do_locator_action(self, track, xclip, ident, args, move_loop_too=False):
except:
pass
- def do_loop_action(self, track, xclip, ident, args):
- # type: (None, None, None, Text) -> None
+ def do_loop_action(self, track, xclip, args):
+ # type: (None, None, Text) -> None
'''Handle arrange loop action.'''
args = args.strip()
if not args or args.upper() in KEYWORDS:
@@ -900,7 +900,7 @@ def move_loop_by_factor(self, args):
if args == '<':
factor = -(factor)
elif len(args) > 1:
- factor = self._parent.get_adjustment_factor(args, True)
+ factor = self.get_adjustment_factor(args, True)
new_start = self.song().loop_start + factor
if new_start < 0.0:
new_start = 0.0
@@ -915,146 +915,3 @@ def set_new_loop_position(self, new_start, new_length):
if 0 <= new_start and 0 <= new_length <= self.song().song_length:
self.song().loop_start = new_start
self.song().loop_length = new_length
-
-# region SCENES
-
- def create_scene(self, track, xclip, ident, value=None):
- # type: (None, Clip, None, Optional[Text]) -> None
- '''Creates scene at end of scene list or at the specified index.
- '''
- current_name = None
- if isinstance(xclip, Clip):
- current_name = xclip.name
- xclip.name = ''
- if value and value.strip():
- try:
- index = int(value) - 1
- if 0 <= index < len(self.song().scenes):
- self.song().create_scene(index)
- except:
- pass
- else:
- self.song().create_scene(-1)
- if current_name:
- self._parent.schedule_message(
- 4, partial(self.refresh_xclip_name, (xclip, current_name))
- )
-
- def duplicate_scene(self, track, xclip, ident, args):
- # type: (None, Clip, None, Text) -> None
- '''Duplicates the given scene.'''
- current_name = None
- if isinstance(xclip, Clip) and args:
- current_name = xclip.name
- xclip.name = ''
- self.song().duplicate_scene(self.get_scene_to_operate_on(xclip, args.strip()))
- if current_name:
- self._parent.schedule_message(
- 4, partial(self.refresh_xclip_name, (xclip, current_name))
- )
-
- def refresh_xclip_name(self, clip_info):
- # type: (Tuple[Clip, str]) -> None
- '''This is used for both dupe and create scene to prevent the
- action from getting triggered over and over again.
- '''
- if clip_info[0]:
- clip_info[0].name = clip_info[1]
-
- def delete_scene(self, track, xclip, ident, args):
- # type: (None, Clip, None, Text) -> None
- '''Deletes the given scene as long as it's not the last scene in
- the set.
- '''
- if len(self.song().scenes) > 1:
- self.song().delete_scene(self.get_scene_to_operate_on(xclip, args.strip()))
-
- def set_scene(self, track, xclip, ident, args):
- # type: (None, Clip, None, Text) -> None
- '''Sets scene to play (doesn't launch xclip).'''
- args = args.strip()
- scene = self.get_scene_to_operate_on(xclip, args)
- if args:
- # don't allow randomization unless more than 1 scene
- if 'RND' in args and len(self.song().scenes) > 1:
- num_scenes = len(self.song().scenes)
- rnd_range = [0, num_scenes]
- if '-' in args:
- rnd_range_data = args.replace('RND', '').split('-')
- if len(rnd_range_data) == 2:
- try:
- new_min = int(rnd_range_data[0]) - 1
- except:
- new_min = 0
- try:
- new_max = int(rnd_range_data[1])
- except:
- 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]
- scene = get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0]
- if scene == self._last_scene_index:
- while scene == self._last_scene_index:
- scene = get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0]
- # don't allow adjustment unless more than 1 scene
- elif args.startswith(('<', '>')) and len(self.song().scenes) > 1:
- factor = self._parent.get_adjustment_factor(args)
- if factor < len(self.song().scenes):
- scene = self._last_scene_index + factor
- if scene >= len(self.song().scenes):
- scene -= len(self.song().scenes)
- elif scene < 0 and abs(scene) >= len(self.song().scenes):
- scene = -(abs(scene) - len(self.song().scenes))
- self._last_scene_index = scene
- for t in self.song().tracks:
- if t.is_foldable or (t.clip_slots[scene].has_clip and t.clip_slots[scene].clip == xclip):
- pass
- else:
- t.clip_slots[scene].fire()
-
- def get_scene_to_operate_on(self, xclip, args):
- # type: (Clip, Text) -> int
- scene = list(self.song().scenes).index(self.song().view.selected_scene)
- if isinstance(xclip, Clip):
- scene = xclip.canonical_parent.canonical_parent.playing_slot_index
- if '"' in 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():
- scene = i
- break
- elif args == 'SEL':
- scene = list(self.song().scenes).index(self.song().view.selected_scene)
- elif args:
- try:
- if 0 <= int(args) < len(self.song().scenes) + 1:
- scene = int(args) - 1
- except:
- pass
- return scene
-
- def setup_scene_listeners(self):
- '''Setup listeners for all scenes in set and check that last
- index is in current scene range.
- '''
- self.remove_scene_listeners()
- scenes = self.song().scenes
- if not 0 < self._last_scene_index < len(scenes):
- self._last_scene_index = list(self.song().scenes).index(self.song().view.selected_scene)
- for i, scene in enumerate(scenes):
- self._scenes_to_monitor.append(scene)
- listener = lambda index=i: self.on_scene_triggered(index)
- if not scene.is_triggered_has_listener(listener):
- scene.add_is_triggered_listener(listener)
-
- def remove_scene_listeners(self):
- for i, scene in enumerate(self._scenes_to_monitor):
- if scene:
- listener = lambda index=i: self.on_scene_triggered(index)
- if scene.is_triggered_has_listener(listener):
- scene.remove_is_triggered_listener(listener)
- self._scenes_to_monitor = []
-
-# endregion
diff --git a/src/clyphx/actions/push.py b/src/clyphx/actions/push.py
index 1a5d500..eb00993 100644
--- a/src/clyphx/actions/push.py
+++ b/src/clyphx/actions/push.py
@@ -14,10 +14,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with ClyphX. If not, see .
-from __future__ import absolute_import, unicode_literals, with_statement
+from __future__ import absolute_import, unicode_literals
from builtins import super, dict, range
from typing import TYPE_CHECKING
+from pushbase.instrument_component import InstrumentComponent
+from pushbase.note_editor_component import NoteEditorComponent
+from Push2.scales_component import ScalesComponent
+
from ..core.xcomponent import ControlSurfaceComponent
from ..core.live import Clip
from ..consts import switch, NOTE_NAMES
@@ -25,9 +29,6 @@
if TYPE_CHECKING:
from typing import Any, Sequence, Text, List, Dict, Optional
-
-
-
UNWRITABLE_INDEXES = (17, 35, 53)
MATRIX_MODES = dict(
@@ -88,12 +89,11 @@ def set_script(self, push_script, is_push2=False):
self._is_push2 = is_push2
if self._script and self._script._components:
for c in self._script._components:
- comp_name = c.__class__.__name__
- if comp_name == 'InstrumentComponent':
+ if isinstance(c, InstrumentComponent):
self._ins_component = c
- elif comp_name.endswith('NoteEditorComponent'):
+ elif isinstance(c, NoteEditorComponent):
self._note_editor = c
- elif comp_name == 'ScalesComponent':
+ elif isinstance(c, ScalesComponent):
self._scales_component = c
if not self._is_push2:
s_mode = self._script._scales_enabler._mode_map['enabled'].mode
@@ -132,7 +132,7 @@ def dispatch_action(self, track, xclip, ident, action, args):
self._handle_scale_action(args.replace('SCL', '').strip(), xclip, ident)
elif args.startswith('SEQ') and self._note_editor:
self._handle_sequence_action(args.replace('SEQ', '').strip())
- elif args == 'DRINS' and self.song().view.selected_track.has_midi_input:
+ elif args == 'DRINS' and self.sel_track.has_midi_input:
with self._script.component_guard():
with self._script._push_injector:
self._script._note_modes.selected_mode = 'instrument'
@@ -225,7 +225,7 @@ def _handle_root_note(self, arg_array):
self._ins_component._note_layout.root_note = NOTE_NAMES.index(arg_array[1])
except KeyError:
if arg_array[1] in ('<', '>'):
- new_root = (self._parent.get_adjustment_factor(arg_array[1])
+ new_root = (self.get_adjustment_factor(arg_array[1])
+ self._ins_component._note_layout.root_note)
if 0 <= new_root < 12:
self._ins_component._note_layout.root_note = new_root
@@ -240,7 +240,7 @@ def _handle_octave(self, arg_array):
def _handle_scale_type(self, arg_array, args):
# type: (Sequence[Text], Text) -> None
if arg_array[1] in ('<', '>'):
- factor = self._parent.get_adjustment_factor(arg_array[1])
+ factor = self.get_adjustment_factor(arg_array[1])
if self._is_push2:
idx = self._scales_component.selected_scale_index + factor
self._scales_component._set_selected_scale_index(idx)
@@ -271,15 +271,16 @@ def _capture_scale_settings(self, xclip, ident):
# type: (Clip, Text) -> None
'''Captures scale settings and writes them to X-Clip's name.'''
if isinstance(xclip, Clip):
- root = str(self._ins_component._note_layout.root_note)
+ layout = self._ins_component._note_layout
+ root = str(layout.root_note)
if self._is_push2:
scl_type = self._scales_component.selected_scale_index
else:
scl_type = str(self._scales_component._scale_list.scrollable_list
.selected_item_index)
octave = '0'
- fixed = str(self._ins_component._note_layout.is_fixed)
- inkey = str(self._ins_component._note_layout.is_in_key)
+ fixed = str(layout.is_fixed)
+ inkey = str(layout.is_in_key)
orient = '0'
xclip.name = '{} Push SCL {} {} {} {} {} {}'.format(
ident, root, scl_type, octave, fixed, inkey, orient
@@ -288,15 +289,16 @@ def _capture_scale_settings(self, xclip, ident):
def _recall_scale_settings(self, arg_array):
# type: (Sequence[Text]) -> None
'''Recalls scale settings from X-Trigger name.'''
+ layout = self._ins_component._note_layout
try:
- self._ins_component._note_layout.root_note = int(arg_array[0])
+ layout.root_note = int(arg_array[0])
if self._is_push2:
self._scales_component._set_selected_scale_index(int(arg_array[1]))
else:
self._scales_component._scale_list.scrollable_list.selected_item_index =\
int(arg_array[1])
- self._ins_component._note_layout.is_fixed = arg_array[3] == 'TRUE'
- self._ins_component._note_layout.is_in_key = arg_array[4] == 'TRUE'
+ layout.is_fixed = arg_array[3] == 'TRUE'
+ layout.is_in_key = arg_array[4] == 'TRUE'
except:
pass
diff --git a/src/clyphx/actions/scene.py b/src/clyphx/actions/scene.py
new file mode 100644
index 0000000..142bc97
--- /dev/null
+++ b/src/clyphx/actions/scene.py
@@ -0,0 +1,162 @@
+from __future__ import absolute_import, unicode_literals
+from builtins import object
+from typing import TYPE_CHECKING
+import logging
+
+if TYPE_CHECKING:
+ from typing import Optional, Text, Tuple
+ from ..core.live import Clip
+
+log = logging.getLogger(__name__)
+
+
+class SceneMixin(object):
+
+ def create_scene(self, track, xclip, value=None):
+ # type: (None, Clip, Optional[Text]) -> None
+ '''Creates scene at end of scene list or at the specified index.
+ '''
+ if isinstance(xclip, Clip):
+ current_name = xclip.name
+ xclip.name = ''
+ else:
+ current_name = None
+ if value and value.strip():
+ try:
+ index = int(value) - 1
+ if 0 <= index < len(self.song().scenes):
+ self.song().create_scene(index)
+ except Exception as e:
+ msg = "Failed to evaluate create_scene value '%s': {%r}"
+ log.error(msg, value, e)
+ else:
+ self.song().create_scene(-1)
+ if current_name:
+ self._parent.schedule_message(
+ 4, partial(self.refresh_xclip_name, (xclip, current_name))
+ )
+
+ def duplicate_scene(self, track, xclip, args):
+ # type: (None, Clip, Text) -> None
+ '''Duplicates the given scene.'''
+ if isinstance(xclip, Clip) and args:
+ current_name = xclip.name
+ xclip.name = ''
+ else:
+ current_name = None
+ self.song().duplicate_scene(self.get_scene_to_operate_on(xclip, args.strip()))
+ if current_name:
+ self._parent.schedule_message(
+ 4, partial(self.refresh_xclip_name, (xclip, current_name))
+ )
+
+ def refresh_xclip_name(self, clip_info):
+ # type: (Tuple[Clip, str]) -> None
+ '''This is used for both dupe and create scene to prevent the
+ action from getting triggered over and over again.
+ '''
+ if clip_info[0]:
+ clip_info[0].name = clip_info[1]
+
+ def delete_scene(self, track, xclip, args):
+ # type: (None, Clip, Text) -> None
+ '''Deletes the given scene as long as it's not the last scene in
+ the set.
+ '''
+ if len(self.song().scenes) > 1:
+ self.song().delete_scene(self.get_scene_to_operate_on(xclip, args.strip()))
+
+ def set_scene(self, track, xclip, args):
+ # type: (None, Clip, Text) -> None
+ '''Sets scene to play (doesn't launch xclip).
+ '''
+ args = args.strip()
+ scene = self.get_scene_to_operate_on(xclip, args)
+ if args:
+ # don't allow randomization unless more than 1 scene
+ if 'RND' in args and len(self.song().scenes) > 1:
+ num_scenes = len(self.song().scenes)
+ rnd_range = [0, num_scenes]
+ if '-' in args:
+ rnd_range_data = args.replace('RND', '').split('-')
+ if len(rnd_range_data) == 2:
+ try:
+ new_min = int(rnd_range_data[0]) - 1
+ except:
+ new_min = 0
+ try:
+ new_max = int(rnd_range_data[1])
+ except:
+ 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]
+ scene = get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0]
+ if scene == self._last_scene_index:
+ while scene == self._last_scene_index:
+ scene = get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0]
+ # don't allow adjustment unless more than 1 scene
+ elif args.startswith(('<', '>')) and len(self.song().scenes) > 1:
+ factor = self.get_adjustment_factor(args)
+ if factor < len(self.song().scenes):
+ scene = self._last_scene_index + factor
+ if scene >= len(self.song().scenes):
+ scene -= len(self.song().scenes)
+ elif scene < 0 and abs(scene) >= len(self.song().scenes):
+ 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)):
+ t.clip_slots[scene].fire()
+
+ def get_scene_to_operate_on(self, xclip, args):
+ # type: (Clip, Text) -> int
+ if args == 'SEL' or not isinstance(xclip, Clip):
+ scene = self.sel_scene
+ else:
+ scene = xclip.canonical_parent.canonical_parent.playing_slot_index
+
+ if '"' in 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():
+ scene = i
+ break
+ elif args and args != 'SEL':
+ try:
+ if 0 <= int(args) < len(self.song().scenes) + 1:
+ scene = int(args) - 1
+ except:
+ pass
+ return scene
+
+# region LISTENERS
+ def on_scene_triggered(self, index):
+ self._last_scene_index = index
+
+ def on_scene_list_changed(self):
+ self.setup_scene_listeners()
+
+ def setup_scene_listeners(self):
+ '''Setup listeners for all scenes in set and check that last
+ index is in current scene range.
+ '''
+ self.remove_scene_listeners()
+ scenes = self.song().scenes
+ if not 0 < self._last_scene_index < len(scenes):
+ self._last_scene_index = self.sel_scene
+ for i, scene in enumerate(scenes):
+ self._scenes_to_monitor.append(scene)
+ listener = lambda index=i: self.on_scene_triggered(index)
+ if not scene.is_triggered_has_listener(listener):
+ scene.add_is_triggered_listener(listener)
+
+ def remove_scene_listeners(self):
+ for i, scene in enumerate(self._scenes_to_monitor):
+ if scene:
+ listener = lambda index=i: self.on_scene_triggered(index)
+ if scene.is_triggered_has_listener(listener):
+ scene.remove_is_triggered_listener(listener)
+ self._scenes_to_monitor = []
+# endregion
diff --git a/src/clyphx/actions/snap.py b/src/clyphx/actions/snap.py
index dd59995..32a80a2 100644
--- a/src/clyphx/actions/snap.py
+++ b/src/clyphx/actions/snap.py
@@ -226,20 +226,22 @@ def recall_track_snapshot(self, name, xclip, disable_smooth=False):
self._parameters_to_smooth = dict()
self._rack_parameters_to_smooth = dict()
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]
if track in self.current_tracks:
track = self.current_tracks[track]
self._recall_mix_settings(track, param_data)
- if (param_data[PLAY_SETTINGS_POS] is not None and
- not track.is_foldable and
- track is not self.song().master_track):
- if param_data[PLAY_SETTINGS_POS] < 0:
+ if (pos is not None and not track.is_foldable
+ and track is not self.song().master_track):
+ if pos < 0:
track.stop_all_clips()
- elif (track.clip_slots[param_data[PLAY_SETTINGS_POS]].has_clip and
- track.clip_slots[param_data[PLAY_SETTINGS_POS]].clip != xclip):
- track.clip_slots[param_data[PLAY_SETTINGS_POS]].fire()
- if param_data[DEVICE_SETTINGS_POS]:
+ elif (track.clip_slots[pos].has_clip
+ and track.clip_slots[pos].clip != xclip):
+ track.clip_slots[pos].fire()
+ if pos:
self._recall_device_settings(track, param_data)
+
if self._is_control_track and self._parameters_to_smooth:
if (not self._control_rack or
(self._control_rack and
@@ -252,47 +254,44 @@ 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.'''
- if param_data[MIX_STD_SETTINGS_POS]:
- pan_value = param_data[MIX_STD_SETTINGS_POS][MIX_PAN_POS]
- if (track.mixer_device.volume.is_enabled and
- param_data[MIX_STD_SETTINGS_POS][MIX_VOL_POS] != -1):
+ std = param_data[MIX_STD_SETTINGS_POS]
+ if std:
+ pan_value = std[MIX_PAN_POS]
+ if (track.mixer_device.volume.is_enabled and std[MIX_VOL_POS] != -1):
self._get_parameter_data_to_smooth(
- track.mixer_device.volume,
- param_data[MIX_STD_SETTINGS_POS][MIX_VOL_POS]
- )
+ track.mixer_device.volume, std[MIX_VOL_POS])
+
if track.mixer_device.panning.is_enabled and not isinstance(pan_value, int):
self._get_parameter_data_to_smooth(
- track.mixer_device.panning,
- param_data[MIX_STD_SETTINGS_POS][MIX_PAN_POS]
- )
+ track.mixer_device.panning, std[MIX_PAN_POS])
+
if track is not self.song().master_track:
- for i in range(len(param_data[MIX_STD_SETTINGS_POS]) - MIX_SEND_START_POS):
- if (i <= len(track.mixer_device.sends) - 1 and
- track.mixer_device.sends[i].is_enabled):
+ sends = track.mixer_device.sends
+ for i in range(len(std) - MIX_SEND_START_POS):
+ if (i <= len(sends) - 1 and sends[i].is_enabled):
self._get_parameter_data_to_smooth(
- track.mixer_device.sends[i],
- param_data[MIX_STD_SETTINGS_POS][MIX_SEND_START_POS+i]
- )
+ sends[i], std[MIX_SEND_START_POS + i])
+
if param_data[1] and track is not self.song().master_track:
- track.mute = param_data[MIX_EXT_SETTINGS_POS][MIX_MUTE_POS]
- track.solo = param_data[MIX_EXT_SETTINGS_POS][MIX_SOLO_POS]
- track.mixer_device.crossfade_assign = param_data[MIX_EXT_SETTINGS_POS][MIX_CF_POS]
+ 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]
def _recall_device_settings(self, track, param_data):
# type: (Track, Mapping[int, Any]) -> None
'''Recalls device related settings.'''
+ settings = param_data[DEVICE_SETTINGS_POS]
for device in track.devices:
- if device.name in param_data[DEVICE_SETTINGS_POS]:
- self._recall_device_snap(device, param_data[DEVICE_SETTINGS_POS][device.name]['params'])
- if (self._include_nested_devices and
- self._parent._can_have_nested_devices and
- device.can_have_chains and
- 'chains' in param_data[DEVICE_SETTINGS_POS][device.name]):
+ if device.name in settings:
+ self._recall_device_snap(device, settings[device.name]['params'])
+ if (self._include_nested_devices
+ and self._parent._can_have_nested_devices
+ and device.can_have_chains
+ and 'chains' in settings[device.name]):
self._recall_nested_device_snap(
- device,
- param_data[DEVICE_SETTINGS_POS][device.name]['chains'],
- )
- del param_data[DEVICE_SETTINGS_POS][device.name]
+ device, settings[device.name]['chains'])
+ del settings[device.name]
def _recall_device_snap(self, device, stored_params):
# type: (Device, Any) -> None
@@ -497,7 +496,8 @@ def _get_parameter_data_to_smooth(self, parameter, new_value):
else:
parameter.value = new_value
- def _get_snap_device_range(self, args, track):
+ @staticmethod
+ def _get_snap_device_range(args, track):
# type: (Text, Track) -> Tuple[int, int]
'''Returns range of devices to snapshot.'''
dev_args = (args.replace('MIX', '').replace('PLAY', '')
diff --git a/src/clyphx/actions/track.py b/src/clyphx/actions/track.py
index e271fa3..162a7e0 100644
--- a/src/clyphx/actions/track.py
+++ b/src/clyphx/actions/track.py
@@ -81,7 +81,7 @@ def create_clip(self, track, xclip, args):
in the selected slot.
'''
if track.has_midi_input:
- slot = list(self.song().scenes).index(self.song().view.selected_scene)
+ slot = self.sel_scene
bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator
length = bar
if args:
@@ -173,7 +173,7 @@ def set_xfade(self, track, xclip, args):
def set_selection(self, track, xclip, args):
# type: (Track, None, Text) -> None
'''Sets track/slot selection.'''
- self.song().view.selected_track = track
+ self.sel_track = track
if track in self.song().tracks:
if args:
try:
@@ -262,14 +262,13 @@ def _get_slot_index_to_play(self, track, xclip, args, allow_empty_slots=False):
slot_to_play = -1
play_slot = track.playing_slot_index
- select_slot = list(self.song().scenes).index(self.song().view.selected_scene)
if not args:
if isinstance(xclip, Clip):
slot_to_play = xclip.canonical_parent.canonical_parent.playing_slot_index
else:
- slot_to_play = play_slot if play_slot >= 0 else select_slot
+ slot_to_play = play_slot if play_slot >= 0 else self.sel_scene
elif args == 'SEL':
- slot_to_play = select_slot
+ slot_to_play = self.sel_scene
# TODO: repeated, check refactoring
# don't allow randomization unless more than 1 scene
elif 'RND' in args and len(self.song().scenes) > 1:
@@ -296,7 +295,7 @@ def _get_slot_index_to_play(self, track, xclip, args, allow_empty_slots=False):
elif args.startswith(('<', '>')) and len(self.song().scenes) > 1:
if track.is_foldable:
return -1
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
if factor < len(self.song().scenes):
# only launch slots that contain clips
if abs(factor) == 1:
@@ -338,25 +337,25 @@ def adjust_preview_volume(self, track, xclip, args):
# type: (Track, None, Text) -> None
'''Adjust/set master preview volume.'''
if track == self.song().master_track:
- self._parent.do_parameter_adjustment(
+ self.adjust_param(
self.song().master_track.mixer_device.cue_volume, args.strip())
def adjust_crossfader(self, track, xclip, args):
# type: (Track, None, Text) -> None
'''Adjust/set master crossfader.'''
if track == self.song().master_track:
- self._parent.do_parameter_adjustment(
+ self.adjust_param(
self.song().master_track.mixer_device.crossfader, args.strip())
def adjust_volume(self, track, xclip, args):
# type: (Track, None, Text) -> None
'''Adjust/set track volume.'''
- self._parent.do_parameter_adjustment(track.mixer_device.volume, args.strip())
+ self.adjust_param(track.mixer_device.volume, args.strip())
def adjust_pan(self, track, xclip, args):
# type: (Track, None, Text) -> None
'''Adjust/set track pan.'''
- self._parent.do_parameter_adjustment(track.mixer_device.panning, args.strip())
+ self.adjust_param(track.mixer_device.panning, args.strip())
def adjust_sends(self, track, xclip, args):
# type: (Track, None, Text) -> None
@@ -365,7 +364,7 @@ def adjust_sends(self, track, xclip, args):
if len(args) > 1:
param = self.get_send_parameter(track, largs[0].strip())
if param:
- self._parent.do_parameter_adjustment(param, largs[1].strip())
+ self.adjust_param(param, largs[1].strip())
def get_send_parameter(self, track, send_string):
# type: (Track, Text) -> Optional[Any]
@@ -429,7 +428,7 @@ def handle_track_routing(self, args, routings, current_routing):
new_routing = routings[current_routing]
args = args.strip()
if args in ('<', '>'):
- factor = self._parent.get_adjustment_factor(args)
+ factor = self.get_adjustment_factor(args)
if 0 <= (current_routing + factor) < len(routings):
new_routing = routings[current_routing + factor]
else:
diff --git a/src/clyphx/clyphx.py b/src/clyphx/clyphx.py
index 4728351..8fb1fd3 100644
--- a/src/clyphx/clyphx.py
+++ b/src/clyphx/clyphx.py
@@ -13,8 +13,8 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with ClyphX. If not, see .
-from __future__ import with_statement, absolute_import, unicode_literals
-from builtins import super, dict, range, map
+from __future__ import absolute_import, unicode_literals
+from builtins import super, dict, range, map, filter, list
from typing import TYPE_CHECKING
from functools import partial
from itertools import chain
@@ -25,7 +25,8 @@
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.parse import SpecParser
+from .core.parse import IdSpecParser, ObjParser
+from .core.xcomponent import XComponent
from .consts import LIVE_VERSION, SCRIPT_INFO
from .extra_prefs import ExtraPrefs
from .user_config import get_user_settings
@@ -78,7 +79,8 @@ def __init__(self, c_instance):
self._PushApcCombiner = None
self._process_xclips_if_track_muted = True
self._user_settings = get_user_settings()
- # self.parse = SpecParser()
+ self.parse_id = IdSpecParser()
+ self.parse_obj = ObjParser()
with self.component_guard():
self.macrobat = Macrobat(self)
self._extra_prefs = ExtraPrefs(self, self._user_settings.prefs)
@@ -105,13 +107,12 @@ def __init__(self, c_instance):
log.info(msg, SCRIPT_INFO, '.'.join(map(str, LIVE_VERSION)))
self.show_message(SCRIPT_INFO)
- from .dev_doc import get_device_params
- from .core.utils import get_user_clyphx_path
- path = get_user_clyphx_path('live_instant_mapping.md')
- log.info('Updating Live Instant Mapping info')
- with open(path, 'w') as f:
- f.write(get_device_params(format='md', tables=True)) # type: ignore
-
+ # from .dev_doc import get_device_params
+ # from .core.utils import get_user_clyphx_path
+ # path = get_user_clyphx_path('live_instant_mapping.md')
+ # log.info('Updating Live Instant Mapping info')
+ # with open(path, 'w') as f:
+ # f.write(get_device_params(format='md', tables=True)) # type: ignore
def disconnect(self):
for attr in (
@@ -165,11 +166,11 @@ def _handle_dispatch_command(self, cmd):
Main dispatch for calling appropriate class of actions, passes
all necessary arguments to class method.
'''
- name = cmd.action_name
-
if not cmd.tracks:
- return # how?
+ return
+ name = cmd.action_name
+ log.info('CMD %s', cmd)
if name.startswith('SNAP'):
self.snap_actions.dispatch_actions(cmd)
elif name.startswith('DEV'):
@@ -180,6 +181,7 @@ def _handle_dispatch_command(self, cmd):
self.device_actions.dispatch_looper_actions(cmd)
elif name in TRACK_ACTIONS:
self.track_actions.dispatch_actions(cmd)
+ # elif name in GLOBAL_ACTIONS or name.startswith('SCENE'):
elif name in GLOBAL_ACTIONS:
self.global_actions.dispatch_action(cmd.to_single())
elif name.startswith('DR'):
@@ -188,12 +190,13 @@ def _handle_dispatch_command(self, cmd):
self.cs_actions.dispatch_action(cmd.to_single())
elif name in self.user_actions._action_dict:
self.dispatch_user_actions(cmd)
- elif name == 'PSEQ' and cmd.args== 'RESET':
+ elif name == 'PSEQ' and cmd.args == 'RESET':
for v in self._play_seq_clips.values():
v[1] = -1
elif name == 'DEBUG':
if isinstance(cmd.xclip, Clip):
- cmd.xclip.name = str(cmd.xclip.name).upper().replace('DEBUG', 'Debugging Activated')
+ name = str(cmd.xclip.name).upper()
+ cmd.xclip.name = name.replace('DEBUG', 'Debugging Activated')
self.start_debugging()
else:
log.error('Not found dispatcher for %r', cmd)
@@ -231,80 +234,140 @@ def handle_m4l_trigger(self, name):
ActionList('[] {}'.format(name)))
def handle_action_list_trigger(self, track, xtrigger):
+ # type: (Track, XTrigger) -> None
+ log.info('ClyphX.handle_action_list_trigger'
+ '(track=%r, xtrigger=%r)', track, xtrigger)
+
+ if xtrigger == None: # TODO: use case?
+ return
+
+ try:
+ self.run_statement(track, xtrigger)
+ except Exception as e:
+ # snap actions parsing not implemented
+ # use legacy method as a fallback
+ log.error("Failed to run statement '%s': %r", xtrigger.name, e)
+ self._handle_action_list_trigger(track, xtrigger)
+
+ def run_statement(self, track, xtrigger):
+ # type: (Track, XTrigger) -> Any
+ stmt = xtrigger.name.strip().upper()
+ spec = self.parse_id(stmt)
+
+ log.info('run_statement: %s', spec)
+ if spec.override:
+ # control reassignment, so pass to control component
+ self.control_component.assign_new_actions(stmt)
+
+ 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
+ elif spec.off != '*':
+ # TODO: select actual list on dispatching
+ spec.on = spec.off
+
+ # lseq: accessible only to X-Clips
+ if spec.seq == 'LSEQ':
+ actions = self._format_action_list(track, spec.on)
+ self._loop_seq_clips[xtrigger.name] = [spec.id, actions]
+ return self.handle_loop_seq_action_list(xtrigger, 0)
+
+ actions = self._format_action_list(track, spec.on)
+
+ # pseq: accessible to any X-Trigger (except for Startup Actions)
+ if spec.seq == 'PSEQ':
+ return self.handle_play_seq_action_list(actions, xtrigger, spec.id)
+
+ for action in actions:
+ # TODO: split in singledispatch per track?
+ command = _DispatchCommand(action['track'],
+ xtrigger,
+ spec.id,
+ action['action'],
+ action['args'])
+ self.handle_dispatch_command(command)
+
+ def _handle_action_list_trigger(self, track, xtrigger):
# type: (Track, XTrigger) -> None
'''Directly dispatches snapshot recall, X-Control overrides and
Seq X-Clips. Otherwise, separates ident from action names,
splits up lists of action names and calls action dispatch.
'''
- log.debug('ClyphX.handle_action_list_trigger'
- '(track=%r, xtrigger=%r)', track, xtrigger)
- if xtrigger == None:
- # TODO: use case?
- return
-
name = xtrigger.name.strip().upper()
- if name and name[0] == '[' and ']' in name:
+ if not (name and name[0] == '[' and ']' in name):
+ return
- # snap action, so pass directly to snap component
- if ' || (' in name and isinstance(xtrigger, Clip) and xtrigger.is_playing:
- # self.snap_actions.recall_track_snapshot(name, xtrigger)
- self.snap_actions.recall_track_snapshot(None, xtrigger)
+ # snap action, so pass directly to snap component
+ # TODO: xtrigger.is_triggered?
+ if ' || (' in name and isinstance(xtrigger, Clip) and xtrigger.is_playing:
+ # self.snap_actions.recall_track_snapshot(name, xtrigger)
+ self.snap_actions.recall_track_snapshot(None, xtrigger)
- # control reassignment, so pass directly to control component
- elif '[[' in name and ']]' in name:
- self.control_component.assign_new_actions(name)
+ # control reassignment, so pass directly to control component
+ elif '[[' in name and ']]' in name:
+ self.control_component.assign_new_actions(name)
- # standard trigger
- else:
- ident = name[name.index('['):name.index(']')+1].strip()
- raw_action_list = name.replace(ident, '', 1).strip()
+ # standard trigger
+ else:
+ ident = name[name.index('['):name.index(']')+1].strip()
+ raw_action_list = name.replace(ident, '', 1).strip()
+ if not raw_action_list:
+ return
+ is_play_seq = False
+ is_loop_seq = False
+
+ # X-Clips can have on and off action lists
+ if isinstance(xtrigger, Clip):
+ raw_action_list = get_xclip_action_list(xtrigger, raw_action_list)
if not raw_action_list:
return
- is_play_seq = False
- is_loop_seq = False
-
- # X-Clips can have on and off action lists
- if isinstance(xtrigger, Clip):
- raw_action_list = get_xclip_action_list(xtrigger, raw_action_list)
- if not raw_action_list:
- return
-
- # check if the trigger is a PSEQ (accessible to any type of X-Trigger)
- if raw_action_list[0] == '(' and '(PSEQ)' in raw_action_list:
- is_play_seq = True
- 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):
- is_loop_seq = True
- raw_action_list = raw_action_list.replace('(LSEQ)', '').strip()
-
- # build formatted action list
- formatted_action_list = [] # List[Dict[Text, Any]]
- for action_ in raw_action_list.split(';'):
- action_data = self.format_action_name(track, action_.strip())
- if action_data:
- formatted_action_list.append(action_data)
-
- # if seq, pass to appropriate function, else call action
- # dispatch for each action in the formatted action list
- if formatted_action_list:
- if is_play_seq:
- self.handle_play_seq_action_list(formatted_action_list, xtrigger, ident)
- elif is_loop_seq:
- self._loop_seq_clips[xtrigger.name] = [ident, formatted_action_list]
- self.handle_loop_seq_action_list(xtrigger, 0)
- else:
- # TODO: split in singledispatch per track?
- for action in formatted_action_list:
- command = _DispatchCommand(action['track'],
- xtrigger,
- ident,
- action['action'],
- action['args'])
- self.handle_dispatch_command(command)
+
+ # check if the trigger is a PSEQ (accessible to any type of X-Trigger)
+ if raw_action_list[0] == '(' and '(PSEQ)' in raw_action_list:
+ is_play_seq = True
+ 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):
+ is_loop_seq = True
+ raw_action_list = raw_action_list.replace('(LSEQ)', '').strip()
+
+ # build formatted action list
+ formatted_action_list = self._format_action_list(track, raw_action_list)
+ # formatted_action_list = [] # List[Dict[Text, Any]]
+ # for action_ in raw_action_list.split(';'):
+ # action_data = self.format_action_name(track, action_.strip())
+ # if action_data:
+ # formatted_action_list.append(action_data)
+
+ # if seq, pass to appropriate function, else call action
+ # dispatch for each action in the formatted action list
+ if formatted_action_list:
+ if is_play_seq:
+ self.handle_play_seq_action_list(formatted_action_list, xtrigger, ident)
+ elif is_loop_seq:
+ self._loop_seq_clips[xtrigger.name] = [ident, formatted_action_list]
+ self.handle_loop_seq_action_list(xtrigger, 0)
+ else:
+ # TODO: split in singledispatch per track?
+ for action in formatted_action_list:
+ command = _DispatchCommand(action['track'],
+ xtrigger,
+ ident,
+ action['action'],
+ action['args'])
+ self.handle_dispatch_command(command)
+
+ def _format_action_list(self, track, alist):
+ # (Text) -> List[Text]
+ alist = [self.format_action_name(track, a) for a in alist]
+ return list(filter(None, alist))
def format_action_name(self, origin_track, origin_name):
# type: (Any, Text) -> Optional[Dict[Text, Any]]
@@ -336,16 +399,15 @@ def handle_loop_seq_action_list(self, xclip, count):
'''Handles sequenced action lists, triggered by xclip looping.
'''
if xclip.name in self._loop_seq_clips:
- if count >= len(self._loop_seq_clips[xclip.name][1]):
- count -= (
- (count // len(self._loop_seq_clips[xclip.name][1]))
- * len(self._loop_seq_clips[xclip.name][1])
- )
- action = self._loop_seq_clips[xclip.name][1][count]
+ entry = self._loop_seq_clips[xclip.name]
+
+ if count >= len(clip[1]):
+ count -= (count // len(entry[1])) * len(entry[1])
+ action = entry[1][count]
# TODO: _SingleDispatch?
command = _DispatchCommand(action['track'],
xclip,
- self._loop_seq_clips[xclip.name][0],
+ entry[0],
action['action'],
action['args'])
self.handle_dispatch_command(command)
@@ -365,76 +427,12 @@ def handle_play_seq_action_list(self, action_list, xclip, ident):
action = self._play_seq_clips[xclip.name][2][self._play_seq_clips[xclip.name][1]]
# TODO: _SingleDispatch?
command = _DispatchCommand(action['track'],
- xclip,
- self._loop_seq_clips[xclip.name][0],
- action['action'],
- action['args'])
+ xclip,
+ self._loop_seq_clips[xclip.name][0],
+ action['action'],
+ action['args'])
self.handle_dispatch_command(command)
- def do_parameter_adjustment(self, param, value):
- # type: (DeviceParameter, Text) -> None
- '''Adjust (>, reset, random, set val) continuous params, also
- handles quantized param adjustment (should just use +1/-1 for
- those).
- '''
- if not param.is_enabled:
- return
- step = (param.max - param.min) / 127
- new_value = param.value
- if value.startswith(('<', '>')):
- factor = self.get_adjustment_factor(value)
- new_value += factor if param.is_quantized else (step * factor)
- elif value == 'RESET' and not param.is_quantized:
- new_value = param.default_value
- elif 'RND' in value and not param.is_quantized:
- rnd_min = 0
- rnd_max = 128
- if value != 'RND' and '-' in value:
- rnd_range_data = value.replace('RND', '').split('-')
- if len(rnd_range_data) == 2:
- try:
- new_min = int(rnd_range_data[0])
- except:
- new_min = 0
- try:
- new_max = int(rnd_range_data[1]) + 1
- except:
- new_max = 128
- if 0 <= new_min and new_max <= 128 and new_min < new_max:
- rnd_min = new_min
- rnd_max = new_max
- rnd_value = (get_random_int(0, 128) * (rnd_max - rnd_min) / 127) + rnd_min
- new_value = (rnd_value * step) + param.min
-
- else:
- try:
- if 0 <= int(value) < 128:
- try:
- new_value = (int(value) * step) + param.min
- except:
- new_value = param.value
- except:
- pass
- if param.min <= new_value <= param.max:
- param.value = new_value
- log.debug('do_parameter_adjustment called on %s, set value to %s',
- param.name, new_value)
-
- def get_adjustment_factor(self, string, as_float=False):
- # type: (Text, bool) -> Union[int, float]
- '''Get factor for use with < > actions.'''
- factor = 1
-
- if len(string) > 1:
- try:
- factor = (float if as_float else int)(string[1:])
- except:
- factor = 1
-
- if string.startswith('<'):
- factor = -(factor)
- return factor
-
def get_track_to_operate_on(self, origin_name):
# type: (Text) -> Tuple[List[Any], Text]
'''Gets track or tracks to operate on.'''
@@ -462,7 +460,8 @@ def get_track_to_operate_on(self, origin_name):
track_index = -1
if spec.startswith(('<', '>')):
try:
- track_index = (self.get_adjustment_factor(spec)
+ # FIXME:
+ track_index = (XComponent.get_adjustment_factor(spec)
+ sel_track_index)
except:
pass
@@ -489,7 +488,8 @@ def get_track_to_operate_on(self, origin_name):
repr_tracklist(result_tracks), result_name)
return (result_tracks, result_name)
- def get_track_index_by_name(self, name, tracks):
+ @staticmethod
+ def get_track_index_by_name(name, tracks):
# type: (Text, Sequence[Any]) -> Text
'''Gets the index(es) associated with the track name(s)
specified in name.
@@ -620,7 +620,7 @@ def perform_startup_actions(self, action_list):
ActionList(action_list))
def setup_tracks(self):
- '''Setup component tracks on ini and track list changes. Also
+ '''Setup component tracks on init and track list changes. Also
call Macrobat's get rack.
'''
for t in self.song().tracks:
diff --git a/src/clyphx/consts.py b/src/clyphx/consts.py
index 2b5541d..463108d 100644
--- a/src/clyphx/consts.py
+++ b/src/clyphx/consts.py
@@ -18,6 +18,8 @@
from typing import TYPE_CHECKING
import logging
+from _Generic.Devices import DEVICE_DICT, DEVICE_BOB_DICT
+
from . import __version__
from .core.live import (GridQuantization,
MixerDevice,
@@ -38,15 +40,11 @@
app.get_minor_version(),
app.get_bugfix_version())
-if LIVE_VERSION < (9, 5, 0):
- raise RuntimeError('Live releases earlier than 9.5 are not supported')
-
+if LIVE_VERSION < (9, 6, 0):
+ raise RuntimeError('Live releases earlier than 9.6 are not supported')
SCRIPT_NAME = 'ClyphX'
-
-SCRIPT_VERSION = __version__
-
-SCRIPT_INFO = '{} v{}'.format(SCRIPT_NAME, '.'.join(map(str, SCRIPT_VERSION)))
+SCRIPT_INFO = '{} v{}'.format(SCRIPT_NAME, '.'.join(map(str, __version__)))
KEYWORDS = dict(ON=1, OFF=0) # type: Mapping[Optional[Text], bool]
@@ -93,6 +91,7 @@ def switch(obj, attr, value, fallback=unset):
# ('COUNT', WarpMode.count),
)) # type: Mapping[Text, int]
+# region STATES
XFADE_STATES = dict(
A = MixerDevice.crossfade_assignments.A,
OFF = MixerDevice.crossfade_assignments.NONE,
@@ -112,7 +111,6 @@ def switch(obj, attr, value, fallback=unset):
OVER = 3.0,
) # type: Mapping[Text, float]
-# region STATES
# TODO: check gq, rq,r_qnrz, and clip_grid uses
# XXX: bars vs beats
GQ_STATES = dict((
@@ -293,4 +291,10 @@ def switch(obj, attr, value, fallback=unset):
MidiVelocity = 'Velocity',
Vinyl = 'Vinyl Distortion',
)
+
+# BoB is B0, device banks have the index of its label 'B1' = 1, ...
+DEVICE_BANKS = dict()
+for device, banks in DEVICE_DICT.items():
+ DEVICE_BANKS[device] = DEVICE_BOB_DICT[device] + banks
+
# endregion
diff --git a/src/clyphx/core/exceptions.py b/src/clyphx/core/exceptions.py
index 6b8a8b6..d06d299 100644
--- a/src/clyphx/core/exceptions.py
+++ b/src/clyphx/core/exceptions.py
@@ -10,3 +10,9 @@ class InvalidSpec(ClyphXception, ValueError):
class InvalidParam(ClyphXception, ValueError):
pass
+
+class InvalidAction(ClyphXception, TypeError):
+ pass
+
+class ActionUnavailable(ClyphXception, TypeError):
+ pass
diff --git a/src/clyphx/core/live.py b/src/clyphx/core/live.py
index a99a5f1..dbf15ed 100644
--- a/src/clyphx/core/live.py
+++ b/src/clyphx/core/live.py
@@ -11,6 +11,7 @@
Chain,
Clip,
CompressorDevice,
+ Conversions,
Device,
DeviceIO,
DeviceParameter,
diff --git a/src/clyphx/core/models.py b/src/clyphx/core/models.py
index ac2358b..dae528d 100644
--- a/src/clyphx/core/models.py
+++ b/src/clyphx/core/models.py
@@ -5,14 +5,17 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
from __future__ import absolute_import, unicode_literals
-from typing import TYPE_CHECKING, NamedTuple, List, Text
+from typing import TYPE_CHECKING, NamedTuple, List, Text, Optional, Any
from builtins import object
from ..consts import MIDI_STATUS
from .utils import repr_slots
+from .parse_notes import parse_pitch, pitch_note
+from .exceptions import InvalidParam
if TYPE_CHECKING:
- from typing import Optional, Dict, Union
+ from typing import Dict, Union
+ from numbers import Integral
Action = NamedTuple('Action', [('tracks', List[Text]),
@@ -27,6 +30,91 @@
('off', List[Action])])
+IdSpec = NamedTuple('Spec', [('id', Text),
+ ('seq', Text),
+ ('on', Text),
+ ('off', Optional[Text]),
+ ('override', bool)])
+
+
+class Pitch(int):
+ '''Coverts either a numeric value or a note + octave string into a
+ constrained integer [0,127] with `note` and `octave` properties.
+
+ `note` and `octave`, if not provided, are lazy evaluated.
+ '''
+ def __new__(cls, value):
+ # type: (Union[Text, Integral]) -> 'Pitch'
+ try: # numeric value
+ self = super().__new__(cls, value)
+ name, octave = None, None
+ except ValueError: # note + octave
+ name, octave, val = parse_pitch(value)
+ self = super().__new__(cls, val)
+ self._name = name
+ self._octave = octave
+ if 0 <= self < 128:
+ return self
+ raise ValueError(int(self))
+
+ @property
+ def name(self):
+ # type: () -> str
+ if self._name is None:
+ self._name, self._octave = pitch_note(self)
+ return self._name
+
+ @property
+ def octave(self):
+ # type: () -> int
+ if self._octave is None:
+ self._name, self._octave = pitch_note(self)
+ return self._octave
+
+ def __add__(self, other):
+ return self.__class__(int(self) + other)
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+ return self.__class__(int(self) - other)
+
+ def __rsub__(self, other):
+ return self.__class__(other - int(self))
+
+ def __mul__(self, other):
+ '''Up `other` octaves.'''
+ return self.__add__(other * 12)
+
+ def __rmul__(self, other):
+ return self.__add__(other * 12)
+
+ def __div__(self, other):
+ '''Down `other` octaves.'''
+ return self.__sub__(other * 12)
+
+ def __rdiv__(self, other):
+ return self.__sub__(other * 12)
+
+ def __bool__(self):
+ return True
+
+ def __repr__(self):
+ return 'Pitch({}, {})'.format(int(self), self)
+
+ def __str__(self):
+ return '{}{}'.format(self.name, self.octave)
+
+
+# TODO
+Note = NamedTuple('Note', [('pitch', int), # TODO: Pitch
+ ('start', float),
+ ('length', Any),
+ ('vel', Any),
+ ('mute', bool)])
+
+
class UserControl(object):
'''
X-Controls defined in user settings.
@@ -46,12 +134,12 @@ class UserControl(object):
def __init__(self, name, type, channel, value, on_actions, off_actions=None):
# type: (Text, Text, Union[Text, int], Union[Text, int], Text, Optional[Text]) -> None
- self.name = name
- self.type = type.upper()
- self.channel = int(channel) - 1
- self.value = int(value)
+ self.name = name
+ self.type = type.upper()
+ self.channel = int(channel) - 1
+ self.value = Pitch(value) if self.type == 'NOTE' else int(value)
# TODO: parse action lists
- self.on_actions = '[{}] {}'.format(name, on_actions.strip())
+ self.on_actions = '[{}] {}'.format(name, on_actions.strip())
if off_actions == '*':
self.off_actions = self.on_actions # type: Optional[Text]
elif off_actions:
@@ -73,7 +161,7 @@ def status_byte(self):
try:
return MIDI_STATUS[self.type]
except KeyError:
- raise ValueError("Message type must be 'NOTE' or 'CC'")
+ raise InvalidParam("Message type must be 'NOTE' or 'CC'")
@classmethod
def parse(cls, name, data):
@@ -87,9 +175,9 @@ def _validate(self):
# TODO: check valid identifier
if not (0 <= self.channel <= 15):
- raise ValueError('MIDI channel must be an integer between 1 and 16')
+ raise InvalidParam('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')
+ if not (0 <= self.value < 128):
+ raise InvalidParam('NOTE or CC must be an integer between 0 and 127')
__repr__ = repr_slots
diff --git a/src/clyphx/core/parse.py b/src/clyphx/core/parse.py
index 6579774..5e48e31 100644
--- a/src/clyphx/core/parse.py
+++ b/src/clyphx/core/parse.py
@@ -5,123 +5,97 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
from __future__ import absolute_import, unicode_literals
+from builtins import map, object, dict
from typing import TYPE_CHECKING
-from builtins import map, object
import re
-from .models import Action, Spec, UserControl
+from .models import IdSpec
from .exceptions import ParsingError
if TYPE_CHECKING:
- from typing import Any, Optional, Text, Iterator, Iterable
-
-
-# region TERMINAL SYMBOLS
-OBJECT_SYMBOLS = {
- 'SEL',
- 'LAST',
- '<',
- '>',
- 'ALL',
- 'MST',
- 'RACKS',
- 'DETAIL',
- 'SELF',
- # return tracks ( index = ord(track) - 65 )
- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
-}
-
-TRACKCNT = r'\b(?P\d{1,2})\b'
-TRACKRET = r'\b(?P[A-La-l])\b'
-TRACKNAM = r'\b\"(?P[\w\s\]+?)\"\b'
-TRACKSYM = r'\b(?P[A-Za-z]\w+?)\b'
-TRACK = r'({}|{}|{}|{})'.format(TRACKCNT, TRACKRET, TRACKNAM, TRACKSYM)
-TRACKRG = r'(?P{}\-{})'.format(TRACK.replace('P<', 'P\w*?)',
+ OVERRIDE = r'(?:\[(?P\w*?)\])', # X-Control override
+ SEQ = r'\(R?(?P[PL]SEQ)\)',
+)
+
+NONTERMINALS = dict(
+ LISTS = r'(?P[\w<>"]\S.*?)',
+ TOKENS = r'(?=[^|;]\s*?)(\S.*?)\s*?(?=$|;)',
+)
+
+SYMBOLS = TERMINALS.copy()
+SYMBOLS.update(NONTERMINALS)
+
+
+class IdSpecParser(object):
+
#: spec structure
spec = re.compile(r'''
- \[(?P\w*?)\]\s*?
- (\((?PR?[PL]SEQ)\))?\s*?
- (?P\S.*?)\s*?$
- ''', re.I | re.X)
+ ^\[({IDENT}|{OVERRIDE})?\]\s*?
+ ({SEQ}\s*)?
+ {LISTS}\s*?$
+ '''.format(**SYMBOLS), re.I | re.X)
#: action lists
lists = re.compile(r'''
(?P[^:]*?)
- (?:\s*?[:]\s*?(?P\S[^:]*?))?\s*?$
+ (?:\s*?[:]\s*?
+ (?P\S[^:]*?))?\s*?$
''', re.X)
+ tokens = re.compile(NONTERMINALS['TOKENS'])
+
+ def _parse(self, string):
+ # type: (Text) -> IdSpec
+
+ # extract identifier and seq
+ spec = self.spec.search(string).groupdict()
+
+ # 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})
+
+ if spec['override']:
+ spec.update(id=spec['override'], override=True)
+ else:
+ spec['override'] = False
- #: list actions
- actions = re.compile(r'^\s+|\s*;\s*|\s+$')
-
- #: action structure
- action = re.compile(
- r'^((?P[\w<>"\-,\s]*?)\s?/)?'
- r'(?P[A-Z0-9]+?)'
- r'(\((?P[A-Z0-9"\-,<>.]+?)\))?'
- r'(\s+?(?P(?! ).*))?$'
- , re.I)
-
- splittracks = re.compile(r'\s?[\\A,\\Z]\s?').split
-
- #: tracks definition
- tracks = re.compile(r'''
- \"(?P[A-Z0-9\-<>\s]+?)?\"|
- (?:^|[\s,]?(?P[A-Z0-9<>]+?)|
- (?P[A-Z0-9\-<>]+?))(?:[\s,]|$) # ranges
- ''', re.I | re.X)
-
- def parse_tracks(self, tracks):
- # type: (Text) -> Optional[Iterable[Text]]
- if not tracks:
- return None
-
- # TODO: split before parsing
- # tracks = self.splittracks(tracks)
-
- return [{k:v for k, v in m.groupdict().items() if v}
- for m in self.tracks.finditer(tracks)]
-
- # TODO: lexing and syntactic validation
- def parse_args(self, args):
- # type: (Text) -> Optional[Iterable[Text]]
- if not args:
- return None
- return args.split()
-
- def parse_action_list(self, actions):
- # type: (Text) -> Iterator[Action]
+ return IdSpec(**spec)
+
+ def __call__(self, string):
+ # type: (Text) -> IdSpec
try:
- for a in self.actions.split(actions):
- action = self.action.match(a).groupdict()
- action.update(tracks=self.parse_tracks(action['tracks']), # type: ignore
- args=self.parse_args(action['args']))
- yield Action(**action)
- except (AttributeError, TypeError):
- raise ValueError("'{}' is not a valid action list".format(actions))
-
- def __call__(self, text):
- # type: (Text) -> Spec
-
- # split the label into 'id', 'seq' (if any) and action 'lists'
- spec = self.spec.search(text).groupdict()
-
- # split previous 'lists' into 'on' and 'off' action lists
- on, off = self.lists.match(spec.pop('lists')).groups()
+ return self._parse(string)
+ except Exception:
+ raise ParsingError(string)
+
+
+TERMINALS = dict(
+ POS = r'(?P\d+?)',
+ SEL = r'(?PSEL)',
+ NAME = r'\"(?P[A-Z0-9\-<>\s]+?)?\"',
+)
+
+
+class ObjParser(object):
+
+ clip = re.compile(r'CLIP({POS}|{SEL}|{NAME})?'.format(**TERMINALS), re.I)
+
+ def _parse(self, kind, string):
+ match = getattr(self, kind).match(string)
+ return {k: v for k, v in match.groupdict().items() if v}
+
+ def __call__(self, kind, string):
+ # type: (Text, Text) -> Dict[Text, Text]
try:
- off = list(self.parse_action_list(off))
- except ValueError as e:
- if not off or off == '*':
- off = off or None
- else:
- # raise ParsingError(e)
- raise Exception(e)
-
- spec.update(on=list(self.parse_action_list(on)), off=off) # type: ignore
- return Spec(**spec)
+ return self._parse(kind, string)
+ except Exception:
+ raise ParsingError(string)
+
+
+__all__ = ['IdSpecParser', 'ObjParser']
+
+del (TERMINALS, NONTERMINALS, SYMBOLS)
diff --git a/src/clyphx/core/parse_notes.py b/src/clyphx/core/parse_notes.py
new file mode 100644
index 0000000..60ebbe3
--- /dev/null
+++ b/src/clyphx/core/parse_notes.py
@@ -0,0 +1,55 @@
+# coding: utf-8
+#
+# Copyright (c) 2020-2021 Nuno André Novo
+# Some rights reserved. See COPYING, COPYING.LESSER
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import absolute_import, unicode_literals
+from typing import TYPE_CHECKING
+from builtins import dict
+import re
+
+if TYPE_CHECKING:
+ from typing import Text
+
+
+NOTE = dict([
+ ('C', 0),
+ ('Db', 1), ('C#', 1),
+ ('D', 2),
+ ('Eb', 3), ('D#', 3),
+ ('E', 4),
+ ('F', 5),
+ ('Gb', 6), ('F#', 6),
+ ('G', 7),
+ ('Ab', 8), ('G#', 8),
+ ('A', 9),
+ ('Bb', 10), ('A#', 10),
+ ('B', 11),
+])
+NOTE_INDEX = dict((v, k) for k, v in NOTE.items()) # returns sharp notes
+
+OCTAVE = ('-2', '-1', '0', '1', '2', '3', '4', '5', '6', '7', '8')
+
+PITCH = re.compile('({})({})'.format('|'.join(NOTE), '|'.join(OCTAVE)), re.I)
+
+
+def parse_pitch(string):
+ # type: (Text) -> (Text, Text)
+ try:
+ name, octave = PITCH.match(string).groups()
+ name = name.upper()
+ value = NOTE[name] + OCTAVE.index(octave) * 12
+ return name, int(octave), value
+ except AttributeError:
+ raise ValueError(string)
+
+
+def pitch_note(pitch):
+ # type: (int) -> (Text, int)
+ return NOTE_INDEX[pitch % 12], int(OCTAVE[pitch // 12])
+
+
+__all__ = ['parse_pitch', 'pitch_note']
+
+del (NOTE, OCTAVE, NOTE_INDEX, PITCH)
diff --git a/src/clyphx/core/utils.py b/src/clyphx/core/utils.py
index 6a54b02..5637451 100644
--- a/src/clyphx/core/utils.py
+++ b/src/clyphx/core/utils.py
@@ -44,8 +44,8 @@ def get_base_path(*items):
def get_user_clyphx_path(*items):
- base = '~/.{}'.format(SCRIPT_NAME.lower())
- base = os.path.expanduser(base)
+ name = '.' + SCRIPT_NAME.lower()
+ base = os.path.join(os.path.expanduser('~'), name)
dest = os.path.join(base, *items)
return os.path.realpath(dest)
@@ -55,3 +55,7 @@ def set_user_profile():
if not os.path.exists(path):
os.mkdir(path)
os.mkdir(os.path.join(path, 'log'))
+
+
+def logger():
+ pass
diff --git a/src/clyphx/core/xcomponent.py b/src/clyphx/core/xcomponent.py
index 7f9cccd..d92e977 100644
--- a/src/clyphx/core/xcomponent.py
+++ b/src/clyphx/core/xcomponent.py
@@ -8,6 +8,7 @@
if TYPE_CHECKING:
from typing import Any
+ from .live import Track
log = logging.getLogger(__name__)
@@ -28,7 +29,8 @@ def disconnect(self):
'''Called by the control surface on disconnect (app closed,
script closed).
'''
- log.debug('Disconnecting %s', getattr(self, 'name', 'a %s' % self.__class__.name))
+ log.debug('Disconnecting %s',
+ getattr(self, 'name', 'a {}'.format(self.__class__.name)))
self._parent = None
super().disconnect()
@@ -47,3 +49,84 @@ def update(self):
def _update(self):
super().update()
+
+ @property
+ def sel_track(self):
+ # type: () -> Track
+ return self.song().view.selected_track
+
+ @sel_track.setter
+ def sel_track(self, track):
+ # type: (Track) -> None
+ self.song().view.selected_track = track
+
+ @property
+ def sel_scene(self):
+ # type: () -> int
+ return list(self.song().scenes).index(self.song().view.selected_scene)
+
+ @staticmethod
+ def do_parameter_adjustment(param, value):
+ # type: (DeviceParameter, Text) -> None
+ '''Adjust (>, reset, random, set val) continuous params, also
+ handles quantized param adjustment (should just use +1/-1 for
+ those).
+ '''
+ if not param.is_enabled:
+ return
+ step = (param.max - param.min) / 127
+ new_value = param.value
+ if value.startswith(('<', '>')):
+ factor = XComponent.get_adjustment_factor(value)
+ new_value += factor if param.is_quantized else (step * factor)
+ elif value == 'RESET' and not param.is_quantized:
+ new_value = param.default_value
+ elif 'RND' in value and not param.is_quantized:
+ rnd_min = 0
+ rnd_max = 128
+ if value != 'RND' and '-' in value:
+ rnd_range_data = value.replace('RND', '').split('-')
+ if len(rnd_range_data) == 2:
+ try:
+ new_min = int(rnd_range_data[0])
+ except:
+ new_min = 0
+ try:
+ new_max = int(rnd_range_data[1]) + 1
+ except:
+ new_max = 128
+ if 0 <= new_min and new_max <= 128 and new_min < new_max:
+ rnd_min = new_min
+ rnd_max = new_max
+ rnd_value = (get_random_int(0, 128) * (rnd_max - rnd_min) / 127) + rnd_min
+ new_value = (rnd_value * step) + param.min
+
+ else:
+ try:
+ if 0 <= int(value) < 128:
+ try:
+ new_value = (int(value) * step) + param.min
+ except:
+ new_value = param.value
+ except:
+ pass
+ if param.min <= new_value <= param.max:
+ param.value = new_value
+ log.debug('do_parameter_adjustment called on %s, set value to %s',
+ param.name, new_value)
+
+ @staticmethod
+ def get_adjustment_factor(string, as_float=False):
+ # type: (Text, bool) -> Union[int, float]
+ '''Get factor for use with < > actions.'''
+ factor = 1
+
+ if len(string) > 1:
+ try:
+ factor = (float if as_float else int)(string[1:])
+ except:
+ factor = 1
+
+ if string.startswith('<'):
+ factor = -(factor)
+ return factor
diff --git a/src/clyphx/dev_doc.py b/src/clyphx/dev_doc.py
index fec463b..116f1a8 100644
--- a/src/clyphx/dev_doc.py
+++ b/src/clyphx/dev_doc.py
@@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals
from typing import TYPE_CHECKING
+# TODO: get from .consts.DEVICE_BANKS
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/extra_prefs.py b/src/clyphx/extra_prefs.py
index 070233d..3fbecdf 100644
--- a/src/clyphx/extra_prefs.py
+++ b/src/clyphx/extra_prefs.py
@@ -45,7 +45,7 @@ def __init__(self, parent, config):
self._clip_record_slot = None # type: Optional[Any]
self._midi_clip_length = config.get('default_inserted_midi_clip_length', False) # type: bool
self._midi_clip_length_slot = None # type: Optional[Any]
- self._last_track = self.song().view.selected_track # type: Track
+ self._last_track = self.sel_track # type: Track
self.on_selected_track_changed()
def disconnect(self):
@@ -61,7 +61,7 @@ def on_selected_track_changed(self):
functions.
'''
super().on_selected_track_changed()
- track = self.song().view.selected_track
+ track = self.sel_track
clip_slot = self.song().view.highlighted_clip_slot
self.remove_listeners()
if self._show_highlight:
@@ -70,9 +70,7 @@ def on_selected_track_changed(self):
(self.song().master_track,))
if self.song().view.selected_track in tracks:
self._parent._set_session_highlight(
- tracks.index(self.song().view.selected_track),
- list(self.song().scenes).index(self.song().view.selected_scene),
- 1, 1, True,
+ tracks.index(self.song().view.selected_track), self.sel_scene, 1, 1, True,
)
else:
self._parent._set_session_highlight(-1, -1, -1, -1, False)
@@ -124,8 +122,7 @@ def clip_record_slot_changed(self):
'''Called on slot has clip changed. Checks if clip is recording
and retriggers it if so.
'''
- track = self.song().view.selected_track
- if self.song().clip_trigger_quantization != 0 and track.arm:
+ if self.song().clip_trigger_quantization != 0 and self.sel_track.arm:
clip = self._clip_record_slot.clip
if clip and clip.is_recording:
clip.fire()
diff --git a/src/clyphx/m4l_browser.py b/src/clyphx/m4l_browser.py
index b443e49..d321d2a 100644
--- a/src/clyphx/m4l_browser.py
+++ b/src/clyphx/m4l_browser.py
@@ -70,7 +70,7 @@ def activate_hotswap(self):
device.
'''
# type: () -> List[Any]
- device = self.song().view.selected_track.view.selected_device
+ device = self.sel_track.view.selected_device
items = []
if device:
if self.application().view.browse_mode:
@@ -157,7 +157,7 @@ def _track_contains_browser(self):
'''Returns whether or not the selected track contains the Device
Browser, in which case hotswapping isn't possble.
'''
- for device in self.song().view.selected_track.devices:
+ for device in self.sel_track.devices:
if device and device.name == 'Device Browser':
return True
return False
@@ -165,7 +165,7 @@ def _track_contains_browser(self):
def _create_devices_for_tag(self, tag):
'''Creates dict of devices for the given tag. Special handling
is needed for M4L tag, which only contains folders, and Drums
- tag, which ontains devices and folders.
+ tag, which contains devices and folders.
'''
# type: (Text) -> Dict[Text, Any]
device_dict = dict()
diff --git a/src/clyphx/macrobat/consts.py b/src/clyphx/macrobat/consts.py
index b682b8a..724f4de 100644
--- a/src/clyphx/macrobat/consts.py
+++ b/src/clyphx/macrobat/consts.py
@@ -29,6 +29,16 @@
('NK CS', MacrobatChainSelectorRack), # param rack
])
+RNR_EXCLUDED = (
+ 'NK RND',
+ 'NK RST',
+ 'NK CHAIN MIX',
+ 'NK DR',
+ 'NK LEARN',
+ 'NK RECEIVER',
+ 'NK TRACK',
+ 'NK SIDECHAIN',
+)
# {param: (mess_type, reset)}
RNR_ON_OFF = dict([
diff --git a/src/clyphx/macrobat/macrobat.py b/src/clyphx/macrobat/macrobat.py
index f417304..82679c7 100644
--- a/src/clyphx/macrobat/macrobat.py
+++ b/src/clyphx/macrobat/macrobat.py
@@ -73,7 +73,7 @@ def disconnect(self):
super().disconnect()
def update(self):
- if self._track and self.song().view.selected_track == self._track:
+ if self._track and self.sel_track == self._track:
self.setup_devices()
def reallow_updates(self):
diff --git a/src/clyphx/macrobat/midi_rack.py b/src/clyphx/macrobat/midi_rack.py
index 69813e7..5d33aeb 100644
--- a/src/clyphx/macrobat/midi_rack.py
+++ b/src/clyphx/macrobat/midi_rack.py
@@ -117,11 +117,16 @@ def do_sysex(self):
send_new_val = True
for byte in p[1][0]:
if byte == -1:
- new_val = int((((p[1][2] - p[1][1]) / 127.0) * int(p[0].value)) + p[1][1])
- if int((((p[1][2] - p[1][1]) / 127.0) * p[2]) + p[1][1]) != new_val:
+ if int(p[0].value) != p[2]:
+ new_val = int((((p[1][2] - p[1][1]) / 127.0) * int(p[0].value)) + p[1][1])
new_string.append(new_val)
else:
send_new_val = False
+ # new_val = int((((p[1][2] - p[1][1]) / 127.0) * int(p[0].value)) + p[1][1])
+ # if int((((p[1][2] - p[1][1]) / 127.0) * p[2]) + p[1][1]) != new_val:
+ # new_string.append(new_val)
+ # else:
+ # send_new_val = False
else:
new_string.append(byte)
if send_new_val:
@@ -155,7 +160,8 @@ def check_sysex_list(self, name_string):
result = entry[1:4]
return result
- def check_for_channel(self, name):
+ @staticmethod
+ def check_for_channel(name):
# type: (Text) -> int
'''Check for user-specified channel in rack name.
'''
@@ -171,7 +177,8 @@ def check_for_channel(self, name):
log.error("Invalid MIDI channel number: '%d'", value)
return 0
- def check_for_cc_num(self, name):
+ @staticmethod
+ def check_for_cc_num(name):
# type: (Text) -> Optional[int]
'''Check for user-specified CC# in macro name.
'''
diff --git a/src/clyphx/macrobat/parameter_rack_template.py b/src/clyphx/macrobat/parameter_rack_template.py
index 250ed8f..7ebb8a2 100644
--- a/src/clyphx/macrobat/parameter_rack_template.py
+++ b/src/clyphx/macrobat/parameter_rack_template.py
@@ -57,11 +57,13 @@ def setup_device(self, rack):
self._on_off_param = [rack.parameters[0], rack.parameters[0].value]
rack.parameters[0].add_value_listener(self.on_off_changed)
- def scale_macro_value_to_param(self, macro, param):
+ @staticmethod
+ def scale_macro_value_to_param(macro, param):
# type: (DeviceParameter, DeviceParameter) -> float
return (((param.max - param.min) / 127.0) * macro.value) + param.min
- def scale_param_value_to_macro(self, param):
+ @staticmethod
+ def scale_param_value_to_macro(param):
# type: (DeviceParameter) -> int
return int(((param.value - param.min) / (param.max - param.min)) * 127.0)
diff --git a/src/clyphx/macrobat/parameter_racks.py b/src/clyphx/macrobat/parameter_racks.py
index 6582631..dd2e04a 100644
--- a/src/clyphx/macrobat/parameter_racks.py
+++ b/src/clyphx/macrobat/parameter_racks.py
@@ -247,7 +247,8 @@ def get_sender_macros(self, dev_list):
for c in d.chains:
self.get_sender_macros(c.devices)
- def get_ident_macros(self, rack):
+ @staticmethod
+ def get_ident_macros(rack):
# type: (RackDevice) -> Sequence[Tuple[Text, DeviceParameter]]
'''Get send and receiver macros.'''
macros = []
@@ -433,7 +434,8 @@ def disconnect(self):
setattr(self, attr, None)
super().disconnect()
- def scale_macro_value_to_param(self, macro, param):
+ @staticmethod
+ def scale_macro_value_to_param(macro, param):
return (((param.max - param.min) / 126.0) * macro.value) + param.min
def setup_device(self, rack):
diff --git a/src/clyphx/macrobat/push_rack.py b/src/clyphx/macrobat/push_rack.py
index 2d4ef6b..8428503 100644
--- a/src/clyphx/macrobat/push_rack.py
+++ b/src/clyphx/macrobat/push_rack.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with ClyphX. If not, see .
-from __future__ import with_statement, absolute_import, unicode_literals
+from __future__ import absolute_import, unicode_literals
from builtins import super
from typing import TYPE_CHECKING
@@ -120,7 +120,8 @@ def _update_rack_name(self):
self._push_ins._note_layout.scale.name,
)
- def scale_macro_value_to_param(self, macro, hi_value):
+ @staticmethod
+ def scale_macro_value_to_param(macro, hi_value):
# type: (Any, Number) -> int
'''Scale the value of the macro to the Push parameter being
controlled.
diff --git a/src/clyphx/macrobat/rnr_rack.py b/src/clyphx/macrobat/rnr_rack.py
index ec07932..b9ea99d 100644
--- a/src/clyphx/macrobat/rnr_rack.py
+++ b/src/clyphx/macrobat/rnr_rack.py
@@ -135,6 +135,8 @@ def get_devices_to_operate_on(self, dev_list, devices_to_get):
def get_next_device(self, rnr_rack, dev_list, store_next=False):
# type: (RackDevice, List[RackDevice], bool) -> None
'''Get the next non-RnR device on the track or in the chain.'''
+ from .consts import RNR_EXCLUDED
+
for d in dev_list:
if d and not store_next:
if d == rnr_rack:
@@ -145,10 +147,7 @@ def get_next_device(self, rnr_rack, dev_list, store_next=False):
isinstance(d.canonical_parent, Chain)
):
name = d.name.upper()
- if d and not name.startswith(
- ('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR',
- 'NK LEARN', 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')
- ):
+ if d and not name.startswith(RNR_EXCLUDED):
self._devices_to_operate_on.append(d)
if self._parent._can_have_nested_devices:
if isinstance(rnr_rack.canonical_parent, Chain):
diff --git a/src/clyphx/macrobat/sidechain_rack.py b/src/clyphx/macrobat/sidechain_rack.py
index a3ed444..a8479bd 100644
--- a/src/clyphx/macrobat/sidechain_rack.py
+++ b/src/clyphx/macrobat/sidechain_rack.py
@@ -99,9 +99,8 @@ def update_macros(self, val):
if self._rack:
for p in self._rack.parameters:
name = p.name.upper()
- if name.startswith('DEVICE'):
- if p.value == 0.0:
- return
+ if name.startswith('DEVICE') and p.value == 0.0:
+ return
elif name.startswith('[SC]') and p.is_enabled:
if self._track.output_meter_level == 0.0 or (
self._track.has_audio_output and
diff --git a/src/clyphx/push_apc_combiner.py b/src/clyphx/push_apc_combiner.py
index 246e37c..27b0f77 100644
--- a/src/clyphx/push_apc_combiner.py
+++ b/src/clyphx/push_apc_combiner.py
@@ -16,6 +16,7 @@
from __future__ import absolute_import, unicode_literals
from builtins import super
+from typing import TYPE_CHECKING
from ableton.v2.control_surface.components.session_ring import SessionRingComponent
from ableton.v2.control_surface.elements import TouchEncoderElement
@@ -25,7 +26,6 @@
from .core.xcomponent import XComponent, SessionComponent
-from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Iterable, Optional, Text, Dict, List
from .core.live import MidiRemoteScript
@@ -76,7 +76,8 @@ def set_up_scripts(self, scripts):
self._apc_session.add_offset_listener(self._on_apc_offset_changed)
self._on_apc_offset_changed()
- def _get_session_component(self, script):
+ @staticmethod
+ def _get_session_component(script):
# type: (MidiRemoteScript) -> Optional[Any]
'''Get the session component for the given script.
'''
diff --git a/src/clyphx/triggers/base.py b/src/clyphx/triggers/base.py
index 592ac49..0ff31b1 100644
--- a/src/clyphx/triggers/base.py
+++ b/src/clyphx/triggers/base.py
@@ -23,6 +23,9 @@ def __init__(self, name='none'):
class XTrigger(XComponent):
+ # only available for X-Clips
+ can_loop_seq = False
+
def __init__(self, parent):
# type: (Any) -> None
super().__init__(parent)
@@ -38,4 +41,7 @@ def ref_track(self):
It's the song's selected track for all triggers except for
X-Clips, where is the host track.
'''
- return self._parent.song().selected_track
+ return self.sel_track
+
+ def handle_action_list(self, track, xtrigger):
+ self._parent.handle_action_list_trigger(track, xtrigger)
diff --git a/src/clyphx/triggers/clip.py b/src/clyphx/triggers/clip.py
index bb94865..17e4ff8 100644
--- a/src/clyphx/triggers/clip.py
+++ b/src/clyphx/triggers/clip.py
@@ -5,7 +5,7 @@
import logging
if TYPE_CHECKING:
- from typing import Text
+ from typing import Optional, Text
from ..core.live import Clip
from .base import XTrigger
@@ -17,7 +17,7 @@ def get_xclip_action_list(xclip, full_action_list):
# type: (Clip, Text) -> Text
'''Get the action list to perform.
- X-Clips can have an on and off action list separated by a comma.
+ X-Clips can have an on and off action list separated by a colon.
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.
@@ -41,8 +41,11 @@ def get_xclip_action_list(xclip, full_action_list):
class XClip(XTrigger):
-
+ '''A control on a Session View Clip.
+ '''
can_have_off_list = True
+ can_loop_seq = True
+
__module__ = __name__
def __init__(self, parent, clip):
@@ -54,6 +57,23 @@ def __init__(self, parent, clip):
def update(self):
super().update()
+ @property
+ def slot(self):
+ # type: () -> int
+ return self._clip.canonical_parent.canonical_parent.playing_slot_index
+ # return self.ref_track.playing_slot_index
+
+ @property
+ def actions(self):
+ # type: () -> Optional[Text]
+ # TODO: split actions
+ if self.is_playing:
+ return self.spec.on
+ elif self.spec.off == '*':
+ return self.spec.on
+ elif self.spec.off:
+ return self.spec.off
+
@property
def is_playing(self):
# type: () -> bool
@@ -77,3 +97,4 @@ def ref_track(self):
X-Clips, where is the host track.
'''
raise NotImplementedError
+ # return self._clip.canonical_parent.canonical_parent
diff --git a/src/clyphx/triggers/control.py b/src/clyphx/triggers/control.py
index b31d354..c2f4176 100644
--- a/src/clyphx/triggers/control.py
+++ b/src/clyphx/triggers/control.py
@@ -15,6 +15,8 @@
class XControlComponent(XTrigger):
'''A control on a MIDI controller.
'''
+ can_have_off_list = True
+
__module__ = __name__
def __init__(self, parent):
@@ -95,8 +97,7 @@ def receive_midi(self, bytes):
ctrl_data = self._control_list[(bytes[0], bytes[1])]
ctrl_data['name'].name = ctrl_data['on_action']
if ctrl_data:
- self._parent.handle_action_list_trigger(self.song().view.selected_track,
- ctrl_data['name'])
+ self.handle_action_list(self.ref_rack, ctrl_data['name'])
def get_user_controls(self, settings, midi_map_handle):
# type: (Dict[Text, Text], int) -> None
diff --git a/src/clyphx/triggers/cue.py b/src/clyphx/triggers/cue.py
index 68011d1..e51a421 100644
--- a/src/clyphx/triggers/cue.py
+++ b/src/clyphx/triggers/cue.py
@@ -84,8 +84,7 @@ def set_x_point_time_to_watch(self):
self._x_point_time_to_watch_for = -1
def schedule_x_point_action_list(self, point):
- self._parent.handle_action_list_trigger(self.song().view.selected_track,
- self._x_points[point])
+ self.handle_action_list(self.ref_track, self._x_points[point])
def remove_cue_point_listeners(self):
for cp in self.song().cue_points:
diff --git a/src/clyphx/triggers/track.py b/src/clyphx/triggers/track.py
index c6104d2..4867e8f 100644
--- a/src/clyphx/triggers/track.py
+++ b/src/clyphx/triggers/track.py
@@ -47,15 +47,12 @@ def play_slot_index_changed(self):
new_clip = self.get_xclip(self._track.playing_slot_index)
prev_clip = self.get_xclip(self._last_slot_index)
self._last_slot_index = self._track.playing_slot_index
- if new_clip and prev_clip and new_clip == prev_clip:
- self._triggered_clips.append(new_clip)
- elif new_clip:
- if prev_clip:
- self._triggered_clips.append(prev_clip)
- self._triggered_clips.append(new_clip)
- elif prev_clip:
+ if prev_clip:
self._triggered_clips.append(prev_clip)
+ if new_clip and new_clip != prev_clip:
+ self._triggered_clips.append(new_clip)
self._clip = new_clip
+ # FIXME
if (self._clip and '(LSEQ)' in self._clip.name.upper() and
not self._clip.loop_jump_has_listener(self.on_loop_jump)):
self._clip.add_loop_jump_listener(self.on_loop_jump)
@@ -68,6 +65,7 @@ def get_xclip(self, slot_index):
slot = self._track.clip_slots[slot_index]
if slot.has_clip and not slot.clip.is_recording and not slot.clip.is_triggered:
clip_name = slot.clip.name
+ # TODO: make XClip
if len(clip_name) > 2 and clip_name[0] == '[' and ']' in clip_name:
clip = slot.clip
return clip
diff --git a/src/clyphx/user_actions.py b/src/clyphx/user_actions.py
index 27071d3..534bf8b 100644
--- a/src/clyphx/user_actions.py
+++ b/src/clyphx/user_actions.py
@@ -145,8 +145,8 @@ def example_action_one(self, track, args):
the action list as a string(proceeded by an identifier).
'''
log.info('example_action_one triggered with args=%s', args)
- self._parent.handle_action_list_trigger(self.song().view.selected_track,
- ActionList('[] METRO {}'.format(args)))
+ self._parent.handle_action_list_trigger(
+ self.sel_track,ActionList('[] METRO {}'.format(args)))
def example_action_two(self, track, args):
# type: (Track, Text) -> None
diff --git a/src/clyphx/user_config.py b/src/clyphx/user_config.py
index 9625859..6315816 100644
--- a/src/clyphx/user_config.py
+++ b/src/clyphx/user_config.py
@@ -4,7 +4,7 @@
# Some rights reserved. See COPYING, COPYING.LESSER
# SPDX-License-Identifier: LGPL-2.1-or-later
-from __future__ import absolute_import, unicode_literals, with_statement
+from __future__ import absolute_import, unicode_literals
from typing import TYPE_CHECKING
from builtins import dict, object
import logging
@@ -165,7 +165,7 @@ class UserVars(object):
# new format: %VARNAME%
re_var = re.compile(r'%(\w+?)%')
- # legacy format: $VARNAME
+ # legacy format: $VARNAME
re_legacy_var = re.compile(r'\$(\w+?)\b')
def __getitem__(self, key):