class HardwareSettings(SerializableListenableProperties): min_led_brightness = MIN_USER_FACING_LED_BRIGHTNESS max_led_brightness = 127 led_brightness = listenable_property.managed(max_led_brightness) min_display_brightness = MIN_USER_FACING_DISPLAY_BRIGHTNESS max_display_brightness = MAX_DISPLAY_BRIGHTNESS display_brightness = listenable_property.managed(max_display_brightness)
class NoteLayout(Subject): is_horizontal = listenable_property.managed(True) interval = listenable_property.managed(3) def __init__(self, song=None, preferences=dict(), *a, **k): raise liveobj_valid(song) or AssertionError super(NoteLayout, self).__init__(*a, **k) self._song = song self._scale = self._get_scale_from_name(self._song.scale_name) self._preferences = preferences self._is_in_key = self._preferences.setdefault('is_in_key', True) self._is_fixed = self._preferences.setdefault('is_fixed', False) @property def notes(self): return self.scale.to_root_note(self.root_note).notes @listenable_property def root_note(self): return self._song.root_note @root_note.setter def root_note(self, root_note): self._song.root_note = root_note self.notify_root_note(root_note) @listenable_property def scale(self): return self._scale @scale.setter def scale(self, scale): self._scale = scale self._song.scale_name = scale.name self.notify_scale(self._scale) @listenable_property def is_in_key(self): return self._is_in_key @is_in_key.setter def is_in_key(self, is_in_key): self._is_in_key = is_in_key self._preferences['is_in_key'] = self._is_in_key self.notify_is_in_key(self._is_in_key) @listenable_property def is_fixed(self): return self._is_fixed @is_fixed.setter def is_fixed(self, is_fixed): self._is_fixed = is_fixed self._preferences['is_fixed'] = self._is_fixed self.notify_is_fixed(self._is_fixed) def _get_scale_from_name(self, name): return find_if(lambda scale: scale.name == name, SCALES) or DEFAULT_SCALE
class PadVelocityCurveSettings(SerializableListenableProperties): sensitivity = listenable_property.managed(5) min_sensitivity = 0 max_sensitivity = 10 gain = listenable_property.managed(5) min_gain = 0 max_gain = 10 dynamics = listenable_property.managed(5) min_dynamics = 0 max_dynamics = 10
class FixedLengthSetting(Subject): option_names = LENGTH_OPTION_NAMES selected_index = listenable_property.managed(0) enabled = listenable_property.managed(False) def get_selected_length(self, song): index = self.selected_index length = 2.0**index quant = LENGTH_OPTIONS[index] if index > 1: length = length * song.signature_numerator / song.signature_denominator return (length, quant)
class GridResolution(ControlManager): quantization_buttons = control_list( RadioButtonControl, checked_color='NoteEditor.QuantizationSelected', unchecked_color='NoteEditor.QuantizationUnselected', control_count=8) index = listenable_property.managed(DEFAULT_INDEX) def __init__(self, *a, **k): (super(GridResolution, self).__init__)(*a, **k) self.index = DEFAULT_INDEX self.quantization_buttons[self.index].is_checked = True @property def step_length(self): return old_div(QUANTIZATION_LIST[self.index], QUANTIZATION_FACTOR) @property def clip_grid(self): return CLIP_VIEW_GRID_LIST[self.index] @property def clip_length(self): return CLIP_LENGTH_LIST[self.index] @quantization_buttons.checked def quantization_buttons(self, button): self.index = button.index
class TrackStateColorIndicator(EventObject): color = listenable_property.managed(b'DefaultButton.On') def __init__(self, item_provider=None, track_property=None, property_active_color=None, song=None, *a, **k): super(TrackStateColorIndicator, self).__init__(*a, **k) self._provider = item_provider self._active_color = property_active_color self._property = track_property self._song = song self.__on_items_changed.subject = item_provider self.register_slot(MultiSlot(listener=self.__on_property_changed, event_name_list=( b'selected_item', track_property), subject=item_provider)) self._update_color() @listens(b'items') def __on_items_changed(self): self._update_color() def __on_property_changed(self): self._update_color() def _update_color(self): selected_mixable = self._provider.selected_item use_active_color = liveobj_valid(selected_mixable) and selected_mixable != self._song.master_track and getattr(selected_mixable, self._property) self.color = self._active_color if use_active_color else b'DefaultButton.On'
class FirmwareUpdateComponent(Component): state = listenable_property.managed('welcome') def __init__(self, *a, **k): (super(FirmwareUpdateComponent, self).__init__)(a, is_enabled=False, **k) self._firmware = None def start(self, firmware): logger.info('Start firmware update using %r', firmware.filename) self._firmware = firmware self.notify_firmware_file() self.set_enabled(True) def set_state(): self.state = 'start' self._tasks.add( task.sequence(task.wait(WELCOME_STATE_TIME), task.run(set_state))) def process_firmware_response(self, data): entry = find_if(lambda entry: entry['type'] == 'firmware', data) if entry: self.state = 'success' if entry['success'] else 'failure' @listenable_property def firmware_file(self): if self._firmware is not None: return os.path.join(FIRMWARE_PATH, self._firmware.filename) return '' @property def data_file(self): return os.path.join(FIRMWARE_PATH, 'FlashData.bin')
class Router(EventObject): current_target_index = listenable_property.managed(-1) def __init__(self, routing_level = None, routing_direction = None, song = None, *a, **k): assert song is not None assert routing_level is not None assert routing_direction is not None super(Router, self).__init__(*a, **k) self._song = song self._current_target_property = u'%s_routing_%s' % (routing_direction, routing_level) self._register_listeners() self.current_target_index = self._current_target_index() return def _register_listeners(self): self.register_slot(MultiSlot(subject=self._song.view, event_name_list=(u'selected_track', self._current_target_property), listener=self.__on_current_routing_changed)) self.register_slot(MultiSlot(subject=self._song.view, event_name_list=(u'selected_track', u'available_%ss' % self._current_target_property), listener=self.__on_routings_changed)) @listenable_property def routing_targets(self): return self._get_targets() def _current_target_index(self): try: return self._get_targets().index(self._get_current_target()) except ValueError: return -1 @property def current_target(self): return self._get_current_target() @current_target.setter def current_target(self, new_target): self._set_current_target(new_target) def __on_current_routing_changed(self): self.current_target_index = self._current_target_index() def __on_routings_changed(self): self.notify_routing_targets() def _get_routing_host(self): return self._song.view.selected_track def _get_targets(self): raise NotImplementedError def _get_current_target(self): return getattr(self._get_routing_host(), self._current_target_property) def _set_current_target(self, new_target_id): setattr(self._get_routing_host(), self._current_target_property, new_target_id) @listenable_property def has_input_channel_position(self): return False
class SetupComponent(ModesComponent): category_radio_buttons = control_list(RadioButtonControl, checked_color='Option.Selected', unchecked_color='Option.Unselected') make_it_go_boom_button = ButtonControl() make_it_go_boom = listenable_property.managed(False) def __init__(self, settings=None, pad_curve_sender=None, firmware_switcher=None, *a, **k): assert settings is not None super(SetupComponent, self).__init__(*a, **k) self._settings = settings self._pad_curve_sender = pad_curve_sender has_option = self.application.has_option self.make_it_go_boom_button.enabled = not has_option('_Push2DeveloperMode') and has_option('_MakePush2GoBoom') self._general = GeneralSettingsComponent(parent=self, settings=settings.general, hardware_settings=settings.hardware, is_enabled=False) self._info = InfoComponent(parent=self, firmware_switcher=firmware_switcher, is_enabled=False) self._pad_settings = PadSettingsComponent(parent=self, pad_settings=settings.pad_settings, is_enabled=False) self._display_debug = DisplayDebugSettingsComponent(parent=self, settings=settings.display_debug, is_enabled=False) self.add_mode('Settings', [self._general, self._pad_settings]) self.add_mode('Info', [self._info]) if self.application.has_option('_Push2DeveloperMode'): self.add_mode('Display Debug', [self._display_debug]) self.selected_mode = 'Settings' self.category_radio_buttons.control_count = len(self.modes) self.category_radio_buttons.checked_index = 0 return @make_it_go_boom_button.pressed def make_it_go_boom_button(self, _button): self.make_it_go_boom = True @property def general(self): return self._general @property def info(self): return self._info @property def pad_settings(self): return self._pad_settings @property def display_debug(self): return self._display_debug @property def settings(self): return self._settings @property def velocity_curve(self): return self._pad_curve_sender @category_radio_buttons.checked def category_radio_buttons(self, button): self.selected_mode = self.modes[button.index]
class PadVelocityCurveSender(Component): SEND_RATE = 0.5 curve_points = listenable_property.managed([]) def __init__(self, curve_sysex_element = None, threshold_sysex_element = None, settings = None, chunk_size = None, *a, **k): raise curve_sysex_element is not None or AssertionError raise threshold_sysex_element is not None or AssertionError raise settings is not None or AssertionError raise chunk_size is not None or AssertionError super(PadVelocityCurveSender, self).__init__(*a, **k) self._curve_sysex_element = curve_sysex_element self._threshold_sysex_element = threshold_sysex_element self._settings = settings self._chunk_size = chunk_size self._send_task = self._tasks.add(task.sequence(task.wait(self.SEND_RATE), task.run(self._on_send_task_finished))).kill() self._settings_changed = False self.register_slot(settings, self._on_setting_changed, 'sensitivity') self.register_slot(settings, self._on_setting_changed, 'gain') self.register_slot(settings, self._on_setting_changed, 'dynamics') self._update_curve_model() return def send(self): self._send_velocity_curve() self._send_thresholds() self._settings_changed = False def _send_velocity_curve(self): velocities = self._generate_curve() velocity_chunks = chunks(velocities, self._chunk_size) for index, velocities in enumerate(velocity_chunks): self._curve_sysex_element.send_value(index * self._chunk_size, velocities) def _send_thresholds(self): threshold_values = generate_thresholds(self._settings.sensitivity, self._settings.gain, self._settings.dynamics) self._threshold_sysex_element.send_value(*threshold_values) def _generate_curve(self): return generate_velocity_curve(self._settings.sensitivity, self._settings.gain, self._settings.dynamics) def _on_setting_changed(self, _): if not self._send_task.is_running: self.send() self._send_task.restart() else: self._settings_changed = True self._update_curve_model() def _update_curve_model(self): self.curve_points = self._generate_curve()[:LAST_INDEX_FOR_DISPLAY] def _on_send_task_finished(self): if self._settings_changed: self.send()
class DisplayDebugSettings(SerializableListenableProperties): show_row_spaces = listenable_property.managed(False) show_row_margins = listenable_property.managed(False) show_row_middle = listenable_property.managed(False) show_button_spaces = listenable_property.managed(False) show_unlit_button = listenable_property.managed(False) show_lit_button = listenable_property.managed(False)
class RoutingChannel(RoutingTarget): realtime_channel = listenable_property.managed(None) def __init__(self, realtime_channel=None, *a, **k): super(RoutingChannel, self).__init__(*a, **k) self.realtime_channel = realtime_channel self._layout_names = {Live.Track.RoutingChannelLayout.mono: b'mono', Live.Track.RoutingChannelLayout.midi: b'midi', Live.Track.RoutingChannelLayout.stereo: b'stereo'} @property def layout(self): return self._layout_names[self._live_target.layout]
class FirmwareUpdateComponent(Component): state = listenable_property.managed('welcome') def __init__(self, *a, **k): super(FirmwareUpdateComponent, self).__init__(is_enabled=False, *a, **k) available_firmware_files = self._collect_firmware_files() logger.debug('Available firmware files %r', available_firmware_files) self._latest_firmware = max(available_firmware_files, key=first) def start(self): raise self.state == 'welcome' or AssertionError self.set_enabled(True) def set_state(): self.state = 'start' self._tasks.add( task.sequence(task.wait(WELCOME_STATE_TIME), task.run(set_state))) def process_firmware_response(self, data): if not self.state == 'start': raise AssertionError entry = find_if(lambda entry: entry['type'] == 'firmware', data) self.state = entry and ('success' if entry['success'] else 'failure') def has_newer_firmware(self, major, minor, build): return self.provided_version > FirmwareVersion(major, minor, build) def _collect_firmware_files(self): return filter(lambda x: x[0] is not None, [(extract_firmware_version(f), f) for f in os.listdir(FIRMWARE_PATH) if fnmatch.fnmatch(f, '*.upgrade')]) @property def provided_version(self): return self._latest_firmware[0] @property def firmware_file(self): return os.path.join(FIRMWARE_PATH, self._latest_firmware[1]) @property def data_file(self): return os.path.join(FIRMWARE_PATH, 'FlashData.bin')
class QuantizationSettingsComponent(Component): swing_amount_encoder = EncoderControl() quantize_to_encoder = StepEncoderControl() quantize_amount_encoder = EncoderControl() record_quantization_encoder = StepEncoderControl() record_quantization_toggle_button = ToggleButtonControl( toggled_color='Recording.FixedLengthRecordingOn', untoggled_color='Recording.FixedLengthRecordingOff') quantize_amount = listenable_property.managed(1.0) quantize_to_index = listenable_property.managed(DEFAULT_QUANTIZATION_INDEX) record_quantization_index = listenable_property.managed( DEFAULT_QUANTIZATION_INDEX) def __init__(self, *a, **k): super(QuantizationSettingsComponent, self).__init__(*a, **k) self.__on_swing_amount_changed.subject = self.song self.__on_record_quantization_changed.subject = self.song self.__on_record_quantization_changed() @property def quantize_to(self): return QUANTIZATION_OPTIONS[self.quantize_to_index] @listenable_property def swing_amount(self): return self.song.swing_amount @listenable_property def record_quantization_enabled(self): return self.record_quantization_toggle_button.is_toggled @property def quantization_option_names(self): return QUANTIZATION_NAMES @swing_amount_encoder.value def swing_amount_encoder(self, value, encoder): self.song.swing_amount = clamp(self.song.swing_amount + value * 0.5, 0.0, 0.5) @staticmethod def _clamp_quantization_index(index): return clamp(index, 0, len(QUANTIZATION_OPTIONS) - 1) @quantize_to_encoder.value def quantize_to_encoder(self, value, encoder): self.quantize_to_index = self._clamp_quantization_index( self.quantize_to_index + value) @quantize_amount_encoder.value def quantize_amount_encoder(self, value, encoder): self.quantize_amount = clamp(self.quantize_amount + value, 0.0, 1.0) @record_quantization_encoder.value def record_quantization_encoder(self, value, encoder): self.record_quantization_index = self._clamp_quantization_index( self.record_quantization_index + value) self._update_record_quantization() @record_quantization_toggle_button.toggled def record_quantization_toggle_button(self, value, button): self._update_record_quantization() @listens('swing_amount') def __on_swing_amount_changed(self): self.notify_swing_amount() @listens('midi_recording_quantization') def __on_record_quantization_changed(self): quant_value = self.song.midi_recording_quantization quant_on = quant_value != RecordingQuantization.rec_q_no_q if quant_value in QUANTIZATION_OPTIONS: self.record_quantization_index = QUANTIZATION_OPTIONS.index( quant_value) self.record_quantization_toggle_button.is_toggled = quant_on self.notify_record_quantization_enabled(quant_on) def _update_record_quantization(self): index = QUANTIZATION_OPTIONS[self.record_quantization_index] self.song.midi_recording_quantization = index if self.record_quantization_toggle_button.is_toggled else RecordingQuantization.rec_q_no_q
class SimplerPositions(EventObject): __events__ = (u'warp_markers', u'before_update_all', u'after_update_all') start = listenable_property.managed(0.0) end = listenable_property.managed(0.0) start_marker = listenable_property.managed(0.0) end_marker = listenable_property.managed(0.0) active_start = listenable_property.managed(0.0) active_end = listenable_property.managed(0.0) loop_start = listenable_property.managed(0.0) loop_end = listenable_property.managed(0.0) loop_fade_in_samples = listenable_property.managed(0.0) env_fade_in = listenable_property.managed(0.0) env_fade_out = listenable_property.managed(0.0) slices = listenable_property.managed([]) selected_slice = listenable_property.managed(SlicePoint(0, 0.0)) use_beat_time = listenable_property.managed(False) def __init__(self, simpler=None, *a, **k): assert simpler is not None super(SimplerPositions, self).__init__(*a, **k) self._simpler = simpler self.__on_active_start_changed.subject = simpler.view self.__on_active_end_changed.subject = simpler.view self.__on_loop_start_changed.subject = simpler.view self.__on_loop_end_changed.subject = simpler.view self.__on_loop_fade_changed.subject = simpler.view self.__on_env_fade_in_changed.subject = simpler.view self.__on_env_fade_out_changed.subject = simpler.view self.__on_selected_slice_changed.subject = simpler.view self.post_sample_changed() return def post_sample_changed(self): self.__on_start_marker_changed.subject = self._simpler.sample self.__on_end_marker_changed.subject = self._simpler.sample self.__on_slices_changed.subject = self._simpler.sample self.__on_warping_changed.subject = self._simpler.sample self.__on_warp_markers_changed.subject = self._simpler.sample self.update_all() def _convert_sample_time(self, sample_time): u""" Converts to beat time, if the sample is warped """ sample = self._simpler.sample if liveobj_valid(sample) and sample.warping: return sample.sample_to_beat_time(sample_time) return sample_time @listens('start_marker') def __on_start_marker_changed(self): if liveobj_valid(self._simpler.sample): self.start_marker = self._convert_sample_time( self._simpler.sample.start_marker) @listens('end_marker') def __on_end_marker_changed(self): if liveobj_valid(self._simpler.sample): self.end_marker = self._convert_sample_time( self._simpler.sample.end_marker) @listens('sample_start') def __on_active_start_changed(self): self.active_start = self._convert_sample_time( self._simpler.view.sample_start) @listens('sample_end') def __on_active_end_changed(self): self.active_end = self._convert_sample_time( self._simpler.view.sample_end) @listens('sample_loop_start') def __on_loop_start_changed(self): self.loop_start = self._convert_sample_time( self._simpler.view.sample_loop_start) @listens('sample_loop_end') def __on_loop_end_changed(self): self.loop_end = self._convert_sample_time( self._simpler.view.sample_loop_end) @listens('sample_loop_fade') def __on_loop_fade_changed(self): self.loop_fade_in_samples = self._simpler.view.sample_loop_fade @listens('sample_env_fade_in') def __on_env_fade_in_changed(self): if liveobj_valid(self._simpler.sample): start_marker = self._simpler.sample.start_marker fade_in_end = start_marker + self._simpler.view.sample_env_fade_in self.env_fade_in = self._convert_sample_time( fade_in_end) - self._convert_sample_time(start_marker) @listens('sample_env_fade_out') def __on_env_fade_out_changed(self): if liveobj_valid(self._simpler.sample): end_marker = self._simpler.sample.end_marker fade_out_start = end_marker - self._simpler.view.sample_env_fade_out self.env_fade_out = self._convert_sample_time( end_marker) - self._convert_sample_time(fade_out_start) @listens('slices') def __on_slices_changed(self): if liveobj_valid(self._simpler.sample): self.slices = [ SlicePoint(s, self._convert_sample_time(s)) for s in self._simpler.sample.slices ] @listens('selected_slice') def __on_selected_slice_changed(self): if liveobj_valid(self._simpler.sample): t = self._convert_sample_time(self._simpler.view.selected_slice) self.selected_slice = SlicePoint(t, t) @listens('warping') def __on_warping_changed(self): self.update_all() @listens('warp_markers') def __on_warp_markers_changed(self): self.update_all() self.notify_warp_markers() def update_all(self): if liveobj_valid(self._simpler.sample): self.notify_before_update_all() self.start = self._convert_sample_time(0) self.end = self._convert_sample_time(self._simpler.sample.length) self.__on_start_marker_changed() self.__on_end_marker_changed() self.__on_active_start_changed() self.__on_active_end_changed() self.__on_loop_start_changed() self.__on_loop_end_changed() self.__on_loop_fade_changed() self.__on_env_fade_in_changed() self.__on_env_fade_out_changed() self.__on_slices_changed() self.__on_selected_slice_changed() self.use_beat_time = self._simpler.sample.warping self.notify_after_update_all()
class ScalesComponent(Component): __events__ = ('close',) navigation_colors = dict(color='Scales.Navigation', disabled_color='Scales.NavigationDisabled') up_button = ButtonControl(repeat=True, **navigation_colors) down_button = ButtonControl(repeat=True, **navigation_colors) right_button = ButtonControl(repeat=True, **navigation_colors) left_button = ButtonControl(repeat=True, **navigation_colors) root_note_buttons = control_list(RadioButtonControl, control_count=len(ROOT_NOTES), checked_color='Scales.OptionOn', unchecked_color='Scales.OptionOff') in_key_toggle_button = ToggleButtonControl(toggled_color='Scales.OptionOn', untoggled_color='Scales.OptionOn') fixed_toggle_button = ToggleButtonControl(toggled_color='Scales.OptionOn', untoggled_color='Scales.OptionOff') scale_encoders = control_list(StepEncoderControl) close_button = ButtonControl(color='Scales.Close') horizontal_navigation = listenable_property.managed(False) NUM_DISPLAY_ROWS = 4 NUM_DISPLAY_COLUMNS = int(ceil(float(len(SCALES)) / NUM_DISPLAY_ROWS)) def __init__(self, note_layout = None, *a, **k): raise note_layout is not None or AssertionError super(ScalesComponent, self).__init__(*a, **k) self._note_layout = note_layout self._scale_list = list(SCALES) self._scale_name_list = map(lambda m: m.name, self._scale_list) self._selected_scale_index = -1 self._selected_root_note_index = -1 self.in_key_toggle_button.connect_property(note_layout, 'is_in_key') self.fixed_toggle_button.connect_property(note_layout, 'is_fixed') self.__on_root_note_changed.subject = self._note_layout self.__on_scale_changed.subject = self._note_layout self.__on_root_note_changed(note_layout.root_note) self.__on_scale_changed(note_layout.scale) def _set_selected_scale_index(self, index): index = clamp(index, 0, len(self._scale_list) - 1) self._note_layout.scale = self._scale_list[index] @down_button.pressed def down_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + 1) @up_button.pressed def up_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index - 1) @left_button.pressed def left_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index - self.NUM_DISPLAY_ROWS) @right_button.pressed def right_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + self.NUM_DISPLAY_ROWS) @root_note_buttons.pressed def root_note_buttons(self, button): self._note_layout.root_note = ROOT_NOTES[button.index] @listens('root_note') def __on_root_note_changed(self, root_note): self._selected_root_note_index = list(ROOT_NOTES).index(root_note) self.root_note_buttons.checked_index = self._selected_root_note_index self.notify_selected_root_note_index() @property def root_note_names(self): return [ NOTE_NAMES[note] for note in ROOT_NOTES ] @listenable_property def selected_root_note_index(self): return self._selected_root_note_index @scale_encoders.value def scale_encoders(self, value, encoder): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + value) @property def scale_names(self): return self._scale_name_list @listenable_property def selected_scale_index(self): return self._selected_scale_index @listens('scale') def __on_scale_changed(self, scale): index = self._scale_list.index(scale) if scale in self._scale_list else -1 if index != self._selected_scale_index: self._selected_scale_index = index self.up_button.enabled = index > 0 self.left_button.enabled = index > 0 self.down_button.enabled = index < len(self._scale_list) - 1 self.right_button.enabled = index < len(self._scale_list) - 1 self.notify_selected_scale_index() @close_button.pressed def close_button(self, button): self.notify_close() @property def note_layout(self): return self._note_layout def _update_horizontal_navigation(self): self.horizontal_navigation = self.right_button.is_pressed or self.left_button.is_pressed
class GeneralSettings(EventObject): workflow = listenable_property.managed(b'scene')
class ProfilingSettings(SerializableListenableProperties): show_qml_stats = listenable_property.managed(False) show_usb_stats = listenable_property.managed(False) show_realtime_ipc_stats = listenable_property.managed(False)
class ClipPositions(EventObject): __events__ = (u'is_recording', u'warp_markers', u'before_update_all', u'after_update_all') MAX_TIME = 10000000 MIN_TIME = -10000 start = listenable_property.managed(0.0) end = listenable_property.managed(0.0) start_marker = listenable_property.managed(0.0) end_marker = listenable_property.managed(0.0) loop_start = listenable_property.managed(0.0) loop_end = listenable_property.managed(0.0) loop_length = listenable_property.managed(0.0) use_beat_time = listenable_property.managed(False) def __init__(self, clip=None, *a, **k): assert clip is not None super(ClipPositions, self).__init__(*a, **k) self._clip = clip self._looping = self._clip.looping self.__on_is_recording_changed.subject = clip self.__on_looping_changed.subject = clip self.__on_start_marker_changed.subject = clip self.__on_end_marker_changed.subject = clip self.__on_loop_start_changed.subject = clip self.__on_loop_end_changed.subject = clip self.__on_loop_start_changed() self.__on_loop_end_changed() if clip.is_audio_clip: self.__on_warping_changed.subject = clip self.__on_warp_markers_changed.subject = clip if clip.is_midi_clip: self.__on_notes_changed.subject = clip self._update_start_end_note_times() self.update_all() @property def is_warping(self): return self._clip.is_audio_clip and self._clip.warping def _convert_to_desired_unit(self, beat_time_or_seconds): u""" This converts the given beat time or seconds unit to the desired unit. - If the input unit is beat time, we are warped and don't need to do anything. - If the input time is seconds, we want to have sample time and need to convert. - If the clip is a midi clip, we don't need to do anything """ if not self._clip.is_midi_clip and not self.is_warping: beat_time_or_seconds = self._clip.seconds_to_sample_time( beat_time_or_seconds) return beat_time_or_seconds @listens(u'start_marker') def __on_start_marker_changed(self): if not self._process_looping_update(): self.start_marker = self._convert_to_desired_unit( self._clip.start_marker) @listens(u'end_marker') def __on_end_marker_changed(self): if not self._process_looping_update(): self.end_marker = self._convert_to_desired_unit( self._clip.end_marker) @listens(u'loop_start') def __on_loop_start_changed(self): if not self._process_looping_update(): self.loop_start = self._convert_to_desired_unit( self._clip.loop_start) self._update_loop_length() @listens(u'loop_end') def __on_loop_end_changed(self): if not self._process_looping_update(): self.loop_end = self._convert_to_desired_unit(self._clip.loop_end) self._update_loop_length() @listens(u'is_recording') def __on_is_recording_changed(self): self._update_start_end() self.notify_is_recording() @listens(u'warp_markers') def __on_warp_markers_changed(self): self.update_all() self.notify_warp_markers() @listens(u'looping') def __on_looping_changed(self): self.update_all() @listens(u'warping') def __on_warping_changed(self): self.update_all() @listens(u'notes') def __on_notes_changed(self): self._update_start_end_note_times() self._update_start_end() def _update_start_end_note_times(self): all_notes = self._clip.get_notes_extended(from_time=self.MIN_TIME, from_pitch=0, time_span=self.MAX_TIME, pitch_span=128) start_times, end_times = list( zip(*[(note.start_time, note.start_time + note.duration) for note in all_notes])) if len(all_notes) > 0 else ( [self.MAX_TIME], [self.MIN_TIME]) self.start_of_first_note = min(start_times) self.end_of_last_note = max(end_times) def _process_looping_update(self): u""" Changing the looping setting is considered a transaction and will update all parameters. This method should be called, before updating any position parameter. Returns True, in case a looping change has been processed. """ looping = self._clip.looping if looping != self._looping: self._looping = looping self.update_all() return True return False def _update_loop_length(self): self.loop_length = self._convert_to_desired_unit( self._clip.loop_end) - self._convert_to_desired_unit( self._clip.loop_start) def _update_start_end(self): start = None end = None if self.is_warping: start = self._clip.sample_to_beat_time(0) end = self._clip.sample_to_beat_time(self._clip.sample_length) elif self._clip.is_audio_clip: start = 0 end = self._clip.sample_length else: start = self.start_of_first_note end = self.end_of_last_note self.start = min( start, self.loop_start if self._clip.looping else self.start_marker) self.end = max(end, self.loop_end) def update_all(self): self.notify_before_update_all() self.__on_start_marker_changed() self.__on_end_marker_changed() self.__on_loop_start_changed() self.__on_loop_end_changed() self._update_start_end() if self._clip.is_audio_clip: self.use_beat_time = self._clip.warping self.notify_after_update_all()
class ConvertComponent(Component): __events__ = (u'cancel', u'success') action_buttons = control_list(ButtonControl, color='Option.Unselected', pressed_color='Option.Selected') cancel_button = ButtonControl(color='Option.Unselected', pressed_color='Option.Selected') source_color_index = listenable_property.managed(UNCOLORED_INDEX) source_name = listenable_property.managed(unicode('')) def __init__(self, tracks_provider=None, conversions_provider=possible_conversions, decorator_factory=None, *a, **k): assert tracks_provider is not None assert callable(conversions_provider) super(ConvertComponent, self).__init__(*a, **k) self._tracks_provider = tracks_provider self._conversions_provider = conversions_provider self._decorator_factory = decorator_factory self._category = NullConvertCategory() self._update_possible_conversions() return @listenable_property def available_conversions(self): return map(lambda x: x.label, self._category.actions) def on_enabled_changed(self): super(ConvertComponent, self).on_enabled_changed() self._update_possible_conversions() def _update_possible_conversions(self): self.disconnect_disconnectable(self._category) track = self._tracks_provider.selected_item self._category = self.register_disconnectable(self._conversions_provider(track, self._decorator_factory)) self.__on_action_invalidated.subject = self._category self.__on_action_source_color_index_changed.subject = self._category.color_source self.__on_action_source_name_changed.subject = self._category.name_source self.__on_action_source_color_index_changed() self.__on_action_source_name_changed() self.action_buttons.control_count = len(self._category.actions) self.notify_available_conversions() @listens('color_index') def __on_action_source_color_index_changed(self): color_source = self.__on_action_source_color_index_changed.subject self.source_color_index = color_source.color_index if color_source and color_source.color_index is not None else UNCOLORED_INDEX return @listens('name') def __on_action_source_name_changed(self): name_source = self.__on_action_source_name_changed.subject self.source_name = name_source.name if name_source else unicode() @action_buttons.released def action_buttons(self, button): if self._do_conversion(button.index): self.notify_cancel() def _do_conversion(self, action_index): self._update_possible_conversions() if action_index < len(self._category.actions): action = self._category.actions[action_index] if action.needs_deferred_invocation: self._tasks.add(task.sequence(task.delay(1), task.run(lambda : self._do_conversion_deferred(action)))) return False self._invoke_conversion(action) return True def _do_conversion_deferred(self, action): self._invoke_conversion(action) self.notify_cancel() def _invoke_conversion(self, action): self._category.convert(self.song, action) self.notify_success(self._category.internal_name) @cancel_button.released def cancel_button(self, button): self.notify_cancel() @listens('action_invalidated') def __on_action_invalidated(self): self.notify_cancel()
class TransportState(CompoundComponent): count_in_duration = listenable_property.managed(0) def __init__(self, song=None, *a, **kw): super(TransportState, self).__init__(*a, **kw) self._song = song self.__on_is_playing_changed.subject = song self._count_in_time_real_time_data = self.register_component( RealTimeDataComponent(channel_type='count-in')) self.__on_count_in_duration_changed.subject = song self.__on_is_counting_in_changed.subject = song self.__on_signature_numerator_changed.subject = song self.__on_signature_denominator_changed.subject = song self.__on_count_in_channel_changed.subject = self._count_in_time_real_time_data self._update_count_in_duration() @listenable_property def count_in_real_time_channel_id(self): return self._count_in_time_real_time_data.channel_id @listenable_property def is_counting_in(self): return self._song.is_counting_in @listenable_property def signature_numerator(self): return self._song.signature_numerator @listenable_property def signature_denominator(self): return self._song.signature_denominator def _update_count_in_duration(self): self.count_in_duration = COUNT_IN_DURATION_IN_BARS[ self._song.count_in_duration] @listens('count_in_duration') def __on_count_in_duration_changed(self): if not self.is_counting_in: self._update_count_in_duration() @listens('is_counting_in') def __on_is_counting_in_changed(self): self._count_in_time_real_time_data.set_data( self._song if self.is_counting_in else None) self.notify_is_counting_in() self._update_count_in_duration() return @listens('signature_numerator') def __on_signature_numerator_changed(self): self.notify_signature_numerator() @listens('signature_denominator') def __on_signature_denominator_changed(self): self.notify_signature_denominator() @listenable_property def is_playing(self): return self._song.is_playing @listens('is_playing') def __on_is_playing_changed(self): self.notify_is_playing() @listens('channel_id') def __on_count_in_channel_changed(self): self.notify_count_in_real_time_channel_id()
class RoutingControlComponent(ModesComponent): monitor_state_encoder = ListValueEncoderControl(num_steps=10) input_output_choice_encoder = ListValueEncoderControl(num_steps=10) routing_type_encoder = ListValueEncoderControl(num_steps=10) routing_channel_encoders = control_list(ListValueEncoderControl, control_count=4, num_steps=10) routing_channel_position_encoder = ListIndexEncoderControl(num_steps=10) can_route = listenable_property.managed(False) @depends(real_time_mapper=None, register_real_time_data=const(nop)) def __init__(self, real_time_mapper=None, register_real_time_data=None, *a, **k): super(RoutingControlComponent, self).__init__(*a, **k) self.__on_current_monitoring_state_changed.subject = self.song.view self._real_time_channel_assigner = RoutingMeterRealTimeChannelAssigner(real_time_mapper=real_time_mapper, register_real_time_data=register_real_time_data, is_enabled=False) self._update_monitoring_state_task = self._tasks.add(task.run(self._update_monitoring_state)) input_type_router = self.register_disconnectable(InputTypeRouter(song=self.song)) output_type_router = self.register_disconnectable(OutputTypeRouter(song=self.song)) input_channel_router = self.register_disconnectable(InputChannelRouter(song=self.song)) output_channel_router = self.register_disconnectable(OutputChannelRouter(song=self.song)) input_channel_and_position_router = self.register_disconnectable(InputChannelAndPositionRouter(input_channel_router, input_type_router)) self._active_type_router = input_type_router self._active_channel_router = input_channel_and_position_router self._can_route = can_set_input_routing self._update_can_route() self._routing_type_list, self._routing_channel_list = self.register_disconnectables([ RoutingTypeList(parent_task_group=self._tasks, router=self._active_type_router), RoutingChannelList(parent_task_group=self._tasks, rt_channel_assigner=self._real_time_channel_assigner, router=self._active_channel_router)]) self.__on_input_channel_position_index_changed.subject = input_channel_and_position_router self._routing_channel_position_list = None self._update_routing_channel_position_list() self.add_mode('input', [ SetAttributeMode(self, '_can_route', can_set_input_routing), partial(self._set_active_routers, input_type_router, input_channel_and_position_router), self._real_time_channel_assigner]) self.add_mode('output', [ SetAttributeMode(self, '_can_route', lambda *a: True), partial(self._set_active_routers, output_type_router, output_channel_router), self._real_time_channel_assigner]) self.selected_mode = 'input' self.__on_selected_track_changed.subject = self.song.view self.__on_selected_track_changed() self._connect_monitoring_state_encoder() self.input_output_choice_encoder.connect_static_list(self, 'selected_mode', list_values=[ 'input', 'output']) self.__on_selected_mode_changed.subject = self self.__on_tracks_changed.subject = self.song self.__on_return_tracks_changed.subject = self.song self._update_track_listeners() return @listenable_property def can_monitor(self): track = self.song.view.selected_track return hasattr(track, 'current_monitoring_state') and not track.is_frozen and track.can_be_armed @listenable_property def monitoring_state_index(self): if self.can_monitor: return self.song.view.selected_track.current_monitoring_state return @listenable_property def is_choosing_output(self): return self.selected_mode == 'output' @listenable_property def routing_type_list(self): return self._routing_type_list @listenable_property def routing_channel_list(self): return self._routing_channel_list @listenable_property def routing_channel_position_list(self): return self._routing_channel_position_list @listens('tracks') def __on_tracks_changed(self): self._update_track_listeners() @listens('return_tracks') def __on_return_tracks_changed(self): self._update_track_listeners() @listens('selected_mode') def __on_selected_mode_changed(self, _): self.notify_is_choosing_output() @listens('selected_track.current_monitoring_state') def __on_current_monitoring_state_changed(self): self.notify_monitoring_state_index() @listens('selected_track') def __on_selected_track_changed(self): self._update_monitoring_state() self._update_can_route() self._update_routing_type_list() self._update_routing_channel_list() self._update_routing_channel_position_list() self._reconnect_selected_track_slots() @listens_group('output_routing_type') def __on_any_output_routing_type_changed(self, *_a): self._update_monitoring_state_task.restart() @listens('is_frozen') def __on_is_frozen_changed(self): self._update_can_monitor() self._update_can_route() @listens('input_channel_position_index') def __on_input_channel_position_index_changed(self): self._update_routing_channel_list() @listens('has_input_channel_position') def __on_has_input_channel_position_changed(self, *a): self._update_routing_channel_position_list() self._connect_input_channel_position_encoder() @listens('input_routing_type') def __on_input_routing_type_changed(self): self._update_can_monitor() def _update_can_route(self): track = self.song.view.selected_track self.can_route = self._can_route(track, self.song) and track != self.song.master_track self._enable_encoders(self.can_route) def _enable_encoders(self, enabled): self.routing_type_encoder.enabled = enabled self.routing_channel_position_encoder.enabled = enabled for encoder in self.routing_channel_encoders: encoder.enabled = enabled def _set_active_routers(self, type_router, channel_router): self._active_type_router = type_router self._active_channel_router = channel_router self._update_can_route() self._update_routing_type_list() self._update_routing_channel_list() self._update_routing_channel_position_list() self._connect_input_channel_position_encoder() self.__on_has_input_channel_position_changed.subject = channel_router def _connect_input_channel_position_encoder(self): if self._active_channel_router.has_input_channel_position: self.routing_channel_position_encoder.connect_list_property(self._active_channel_router, current_index_property_name='input_channel_position_index', max_index=len(self._active_channel_router.input_channel_positions) - 1) self.routing_channel_position_encoder.enabled = self.can_route else: self.routing_channel_position_encoder.enabled = False self.routing_channel_position_encoder.disconnect_property() def _update_routing_type_list(self): self.unregister_disconnectable(self._routing_type_list) self._routing_type_list.disconnect() self._routing_type_list = self.register_disconnectable(RoutingTypeList(parent_task_group=self._tasks, router=self._active_type_router)) self.notify_routing_type_list() self.routing_type_encoder.connect_list_property(self._routing_type_list, current_value_property_name='selected_target', list_property_name='targets') def _update_routing_channel_list(self): self.unregister_disconnectable(self._routing_channel_list) self._routing_channel_list.disconnect() self._routing_channel_list = self.register_disconnectable(RoutingChannelList(parent_task_group=self._tasks, rt_channel_assigner=self._real_time_channel_assigner, router=self._active_channel_router)) self.notify_routing_channel_list() for encoder in self.routing_channel_encoders: encoder.connect_list_property(self._routing_channel_list, current_value_property_name='selected_target', list_property_name='targets') def _update_routing_channel_position_list(self): if self._routing_channel_position_list is not None: self.unregister_disconnectable(self._routing_channel_position_list) self._routing_channel_position_list.disconnect() if self._active_channel_router.has_input_channel_position: self._routing_channel_position_list = self.register_disconnectable(RoutingChannelPositionList(self._active_channel_router)) else: self._routing_channel_position_list = None self.notify_routing_channel_position_list() return def _connect_monitoring_state_encoder(self): self.monitor_state_encoder.connect_static_list(self.song.view.selected_track, 'current_monitoring_state', list_values=[ Live.Track.Track.monitoring_states.IN, Live.Track.Track.monitoring_states.AUTO, Live.Track.Track.monitoring_states.OFF]) def _update_monitoring_state(self): self._update_monitoring_state_task.kill() self._connect_monitoring_state_encoder() self._update_can_monitor() def _update_can_monitor(self): if self.monitor_state_encoder.enabled != self.can_monitor: self.monitor_state_encoder.enabled = self.can_monitor self.notify_can_monitor() def _update_track_listeners(self): self.__on_any_output_routing_type_changed.replace_subjects(list(self.song.tracks) + list(self.song.return_tracks)) self.__on_any_output_routing_type_changed() def _reconnect_selected_track_slots(self): selected_track = self.song.view.selected_track self.__on_is_frozen_changed.subject = selected_track self.__on_input_routing_type_changed.subject = selected_track
class InputChannelAndPositionRouter(EventObject): u""" Adapts an InputChannelRouter (and InputTypeRouter). For non-track input types, the input channel interface is passed through unaltered, so this looks exactly like the wrapped InputChannelRouter. For track types, the list of input channels is compressed by combining groups of three (pre-fx, post-fx and post mixer - called "positions") items into a single item in the routing_targets list. The position is then selected with input_channel_position_index. """ has_input_channel_position = listenable_property.managed(False) def __init__(self, input_channel_router=None, input_type_router=None, *a, **k): super(InputChannelAndPositionRouter, self).__init__(*a, **k) self._input_type_router = input_type_router self._input_channel_router = input_channel_router self._input_channel_postfixes = [] self._update_channel_grouping() if self.has_input_channel_position: self._last_input_channel_position_index = self.input_channel_position_index else: self._last_input_channel_position_index = None self.__on_routing_targets_changed.subject = input_channel_router self.__on_current_target_index_changed.subject = input_channel_router self.__on_input_type_changed.subject = input_type_router return @listens('current_target_index') def __on_input_type_changed(self, _): self._update_channel_grouping() self.notify_routing_targets() self.notify_input_channel_position_index() @listens('routing_targets') def __on_routing_targets_changed(self): self._update_channel_grouping() self.notify_routing_targets() @listens('current_target_index') def __on_current_target_index_changed(self, _): if self.has_input_channel_position and self._last_input_channel_position_index != self.input_channel_position_index: self.notify_input_channel_position_index() self._last_input_channel_position_index = self.input_channel_position_index self.notify_current_target_index() @listenable_property def routing_targets(self): u""" Input channels of wrapped InputChannelRouter if has_input_channel_position is false. Input channels of from wrapped InputChannelRouter that are in the "position" input_channel_position if has_input_channel_position is true """ complete_list = self._input_channel_router.routing_targets if self.has_input_channel_position: slice_size = len(self.live_position_postfixes) index_in_complete_list = self._input_channel_router.current_target_index return complete_list[index_in_complete_list % slice_size::slice_size] return complete_list @listenable_property def current_target_index(self): u""" Index in routing_targets of the current_target """ index_in_complete_list = self._input_channel_router.current_target_index if self.has_input_channel_position: slice_size = len(self.live_position_postfixes) return index_in_complete_list // slice_size return index_in_complete_list @listenable_property def current_target(self): u""" Currently selected target """ return self._input_channel_router.current_target @current_target.setter def current_target(self, new_target): self._input_channel_router.current_target = new_target @listenable_property def input_channel_positions(self): u""" List of strings naming the input channel positions. Only use if has_input_channel_position is true. """ return self.live_position_postfixes @property def live_position_postfixes(self): u""" List of postfixes found in the names of Live's routing channels with position. Only use if has_input_channel_position is true. """ assert self.has_input_channel_position return self._input_channel_postfixes @listenable_property def input_channel_position_index(self): u""" Index into input_channel_positions of current channel position. Only use if has_input_channel_position is true. """ assert self.has_input_channel_position slice_size = len(self.live_position_postfixes) return self._input_channel_router.current_target_index % slice_size @input_channel_position_index.setter def input_channel_position_index(self, new_index): assert self.has_input_channel_position complete_list = self._input_channel_router.routing_targets index_in_complete_list = self._input_channel_router.current_target_index slice_size = len(self.live_position_postfixes) self._input_channel_router.current_target = complete_list[index_in_complete_list // slice_size * slice_size + new_index] @property def input_type_name(self): u""" Name of the input type. Only use if has_input_channel_position is true. """ assert self.has_input_channel_position current_target = self._input_type_router.current_target if current_target: return getattr(current_target.attached_object, 'name', '') return '' def _update_channel_grouping(self): attached_object = getattr(self._input_type_router.current_target, 'attached_object', None) original_channels = self._input_channel_router.routing_targets if can_combine_targets(original_channels[:len(AUDIO_CHANNEL_POSITION_POSTFIXES)], AUDIO_CHANNEL_POSITION_POSTFIXES): postfixes = AUDIO_CHANNEL_POSITION_POSTFIXES else: postfixes = MIDI_CHANNEL_POSITION_POSTFIXES has_positions = liveobj_valid(attached_object) and targets_can_be_grouped(original_channels, postfixes) self._input_channel_postfixes = postfixes if has_positions else [] self.has_input_channel_position = has_positions self.notify_input_channel_positions() return
class RoutingMeterRealTimeChannelAssigner(Component): list_index_to_pool_index_mapping = listenable_property.managed({}) def __init__(self, real_time_mapper=None, register_real_time_data=nop, sliding_window_size=None, *a, **k): assert real_time_mapper is not None super(RoutingMeterRealTimeChannelAssigner, self).__init__(*a, **k) if sliding_window_size is None: sliding_window_size = real_time_mapper.METER_POOLSIZE assert sliding_window_size > 0 self._half_window_size = sliding_window_size // 2 self._routing_channels = [] self._selected_index = -1 self.real_time_channels = [ RealTimeDataComponent(parent=self, channel_type='meter', real_time_mapper=real_time_mapper, register_real_time_data=register_real_time_data) for _ in xrange(sliding_window_size) ] return def disconnect(self): super(RoutingMeterRealTimeChannelAssigner, self).disconnect() self._routing_channels = [] @property def selected_index(self): return self._selected_index @selected_index.setter def selected_index(self, index): self._selected_index = index self._update_attachments() @property def routing_channels(self): return self._routing_channels @routing_channels.setter def routing_channels(self, channels): self._routing_channels = channels for rt in self.real_time_channels: rt.set_data(None) self.list_index_to_pool_index_mapping = {} self._update_attachments() return def _update_attachments(self): visible_routing_channels = set(self._visible_routing_channels()) attached_routing_channels = set(self._attached_routing_channels()) to_be_detached = attached_routing_channels - visible_routing_channels to_be_attached = visible_routing_channels - attached_routing_channels for routing_channel in to_be_detached: rt_assignment = find_if(lambda rt: rt.attached_object == routing_channel, self.real_time_channels) rt_assignment.set_data(None) for routing_channel in to_be_attached: free_channel = find_if(lambda rt: rt.attached_object is None, self.real_time_channels) free_channel.set_data(routing_channel) self._update_list_index_to_pool_index_mapping() return def _visible_routing_channels(self): window_start = max(0, self._selected_index - self._half_window_size) window_end = self._selected_index + self._half_window_size + 1 return self._routing_channels[window_start:window_end] def _attached_routing_channels(self): return filter(liveobj_valid, imap(lambda real_time_assignment: real_time_assignment.attached_object, self.real_time_channels)) def _update_list_index_to_pool_index_mapping(self): new_mapping = {} for index, rt_assignment in enumerate(self.real_time_channels): if liveobj_valid(rt_assignment.attached_object): list_index = self._routing_channels.index(rt_assignment.attached_object) new_mapping[list_index] = index self.list_index_to_pool_index_mapping = new_mapping
class BrowserComponent(Component, Messenger): __events__ = (u'loaded', u'close') NUM_ITEMS_PER_COLUMN = 6 NUM_VISIBLE_BROWSER_LISTS = 7 NUM_COLUMNS_IN_EXPANDED_LIST = 3 EXPAND_LIST_TIME = 1.5 REVEAL_PREVIEW_LIST_TIME = 0.2 MIN_TIME = 0.6 MAX_TIME = 1.4 MIN_TIME_TEXT_LENGTH = 30 MAX_TIME_TEXT_LENGTH = 70 up_button = ButtonControl(repeat=True) down_button = ButtonControl(repeat=True) right_button = ButtonControl(repeat=True, **NAVIGATION_COLORS) left_button = ButtonControl(repeat=True, **NAVIGATION_COLORS) back_button = ButtonControl(**NAVIGATION_COLORS) open_button = ButtonControl(**NAVIGATION_COLORS) load_button = ButtonControl(**NAVIGATION_COLORS) close_button = ButtonControl() prehear_button = ToggleButtonControl(toggled_color=u'Browser.Option', untoggled_color=u'Browser.OptionDisabled') scroll_encoders = control_list(StepEncoderControl, num_steps=10, control_count=NUM_VISIBLE_BROWSER_LISTS) scroll_focused_encoder = StepEncoderControl(num_steps=10) scrolling = listenable_property.managed(False) horizontal_navigation = listenable_property.managed(False) list_offset = listenable_property.managed(0) can_enter = listenable_property.managed(False) can_exit = listenable_property.managed(False) context_color_index = listenable_property.managed(-1) context_text = listenable_property.managed(u'') @depends(commit_model_changes=None, selection=None) def __init__(self, preferences = dict(), commit_model_changes = None, selection = None, main_modes_ref = None, *a, **k): assert commit_model_changes is not None super(BrowserComponent, self).__init__(*a, **k) self._lists = [] self._browser = Live.Application.get_application().browser self._current_hotswap_target = self._browser.hotswap_target self._updating_root_items = BooleanContext() self._focused_list_index = 0 self._commit_model_changes = commit_model_changes self._preferences = preferences self._expanded = False self._unexpand_with_scroll_encoder = False self._delay_preview_list = BooleanContext() self._selection = selection self._main_modes_ref = main_modes_ref if main_modes_ref is not None else nop self._load_neighbour_overlay = LoadNeighbourOverlayComponent(parent=self, is_enabled=False) self._content_filter_type = None self._content_hotswap_target = None self._preview_list_task = self._tasks.add(task.sequence(task.wait(self.REVEAL_PREVIEW_LIST_TIME), task.run(self._replace_preview_list_by_task))).kill() self._update_root_items() self._update_navigation_buttons() self._update_context() self.prehear_button.is_toggled = preferences.setdefault(u'browser_prehear', True) self._on_selected_track_color_index_changed.subject = self.song.view self._on_selected_track_name_changed.subject = self.song.view self._on_detail_clip_name_changed.subject = self.song.view self._on_hotswap_target_changed.subject = self._browser self._on_load_next.subject = self._load_neighbour_overlay self._on_load_previous.subject = self._load_neighbour_overlay self._on_focused_item_changed.subject = self self.register_slot(self, self.notify_focused_item, u'focused_list_index') def auto_unexpand(): self.expanded = False self._update_list_offset() self._unexpand_task = self._tasks.add(task.sequence(task.wait(self.EXPAND_LIST_TIME), task.run(auto_unexpand))).kill() @up_button.pressed def up_button(self, button): with self._delay_preview_list(): self.focused_list.select_index_with_offset(-1) self._update_auto_expand() self._update_scrolling() self._update_horizontal_navigation() @up_button.released def up_button(self, button): self._finish_preview_list_task() self._update_scrolling() @down_button.pressed def down_button(self, button): with self._delay_preview_list(): self.focused_list.select_index_with_offset(1) self._update_auto_expand() self._update_scrolling() self._update_horizontal_navigation() @down_button.released def down_button(self, button): self._finish_preview_list_task() self._update_scrolling() @right_button.pressed def right_button(self, button): if self._expanded and self._can_auto_expand() and self._focused_list_index > 0: self.focused_list.select_index_with_offset(self.NUM_ITEMS_PER_COLUMN) self._update_scrolling() self.horizontal_navigation = True elif not self._enter_selected_item(): self._update_auto_expand() @right_button.released def right_button(self, button): self._update_scrolling() @left_button.pressed def left_button(self, button): if self._expanded and self._focused_list_index > 0 and self.focused_list.selected_index >= self.NUM_ITEMS_PER_COLUMN: self.focused_list.select_index_with_offset(-self.NUM_ITEMS_PER_COLUMN) self._update_scrolling() self.horizontal_navigation = True else: self._exit_selected_item() @left_button.released def left_button(self, button): self._update_scrolling() @open_button.pressed def open_button(self, button): self._enter_selected_item() @back_button.pressed def back_button(self, button): self._exit_selected_item() @scroll_encoders.touched def scroll_encoders(self, encoder): list_index = self._get_list_index_for_encoder(encoder) if list_index is not None: try: if self._focus_list_with_index(list_index, crop=False): self._unexpand_with_scroll_encoder = True self._prehear_selected_item() if self.focused_list.selected_item.is_loadable and encoder.index == self.scroll_encoders.control_count - 1: self._update_list_offset() self._on_encoder_touched() except CannotFocusListError: pass @scroll_encoders.released def scroll_encoders(self, encoders): self._on_encoder_released() @scroll_encoders.value def scroll_encoders(self, value, encoder): list_index = self._get_list_index_for_encoder(encoder) if list_index is not None: try: if self._focus_list_with_index(list_index): self._unexpand_with_scroll_encoder = True self._on_encoder_value(value) except CannotFocusListError: pass @scroll_focused_encoder.value def scroll_focused_encoder(self, value, encoder): self._on_encoder_value(value) @scroll_focused_encoder.touched def scroll_focused_encoder(self, encoder): self._on_encoder_touched() @scroll_focused_encoder.released def scroll_focused_encoder(self, encoder): self._on_encoder_released() def _on_encoder_value(self, value): with self._delay_preview_list(): self.focused_list.select_index_with_offset(value) first_visible_list_focused = self.focused_list_index == self.list_offset if self.expanded and first_visible_list_focused: self.expanded = False self._unexpand_with_scroll_encoder = True elif not first_visible_list_focused and not self.expanded and self._can_auto_expand(): self._update_auto_expand() self._unexpand_with_scroll_encoder = True self._update_scrolling() self._update_horizontal_navigation() def _on_encoder_touched(self): self._unexpand_task.kill() self._update_scrolling() self._update_horizontal_navigation() def _on_encoder_released(self): any_encoder_touched = any(map(lambda e: e.is_touched, self.scroll_encoders)) or self.scroll_focused_encoder.is_touched if not any_encoder_touched and self._unexpand_with_scroll_encoder: self._unexpand_task.restart() self._update_scrolling() def _get_list_index_for_encoder(self, encoder): if self.expanded: if encoder.index == 0: return self.list_offset return self.list_offset + 1 index = self.list_offset + encoder.index if self.focused_list_index + 1 == index and self.should_widen_focused_item: index = self.focused_list_index if 0 <= index < len(self._lists): return index else: return None @load_button.pressed def load_button(self, button): self._load_selected_item() @prehear_button.toggled def prehear_button(self, toggled, button): if toggled: self._prehear_selected_item() else: self._browser.stop_preview() self._preferences[u'browser_prehear'] = toggled self.notify_prehear_enabled() @close_button.pressed def close_button(self, button): self.notify_close() @listenable_property def lists(self): return self._lists @listenable_property def focused_list_index(self): return self._focused_list_index @listenable_property def prehear_enabled(self): return self.prehear_button.is_toggled @property def focused_list(self): return self._lists[self._focused_list_index] @listenable_property def focused_item(self): return self.focused_list.selected_item @listenable_property def expanded(self): return self._expanded @property def load_neighbour_overlay(self): return self._load_neighbour_overlay @listenable_property def should_widen_focused_item(self): return self.focused_item.is_loadable and not self.focused_item.is_device @property def context_display_type(self): return u'custom_button' def disconnect(self): super(BrowserComponent, self).disconnect() self._lists = [] self._commit_model_changes = None @expanded.setter def expanded(self, expanded): if self._expanded != expanded: self._expanded = expanded self._unexpand_with_scroll_encoder = False self._update_navigation_buttons() if len(self._lists) > self._focused_list_index + 1: self._lists[self._focused_list_index + 1].limit = self.num_preview_items self.notify_expanded() @listens(u'selected_track.color_index') def _on_selected_track_color_index_changed(self): if self.is_enabled(): self._update_context() self._update_navigation_buttons() @listens(u'selected_track.name') def _on_selected_track_name_changed(self): if self.is_enabled(): self._update_context() @listens(u'detail_clip.name') def _on_detail_clip_name_changed(self): if self.is_enabled(): self._update_context() @listens(u'hotswap_target') def _on_hotswap_target_changed(self): if self.is_enabled(): if not self._switched_to_empty_pad(): self._update_root_items() self._update_context() self._update_list_offset() self._update_load_neighbour_overlay_visibility() else: self._load_neighbour_overlay.set_enabled(False) self._current_hotswap_target = self._browser.hotswap_target @listens(u'focused_item') def _on_focused_item_changed(self): self.notify_should_widen_focused_item() @property def browse_for_audio_clip(self): main_modes = self._main_modes_ref() if main_modes is None: return False has_midi_support = self.song.view.selected_track.has_midi_input return not has_midi_support and u'clip' in main_modes.active_modes def _switched_to_empty_pad(self): hotswap_target = self._browser.hotswap_target is_browsing_drumpad = isinstance(hotswap_target, Live.DrumPad.DrumPad) was_browsing_pad = isinstance(self._current_hotswap_target, Live.DrumPad.DrumPad) return is_browsing_drumpad and was_browsing_pad and len(hotswap_target.chains) == 0 def _focus_list_with_index(self, index, crop = True): u""" Focus the list with the given index. Raises CannotFocusListError if the operation fails. Returns True if a new list was focused and False if it was already focused. """ if self._focused_list_index != index: if self._finish_preview_list_task(): if index >= len(self._lists): raise CannotFocusListError() assert 0 <= index < len(self._lists) self._on_focused_selection_changed.subject = None if self._focused_list_index > index and crop: for l in self._lists[self._focused_list_index:]: l.selected_index = -1 self._focused_list_index = index self.focused_list.limit = -1 if self.focused_list.selected_index == -1: self.focused_list.selected_index = 0 self.notify_focused_list_index() self._on_focused_selection_changed.subject = self.focused_list if crop: self._crop_browser_lists(self._focused_list_index + 2) if self._focused_list_index == len(self._lists) - 1: self._replace_preview_list() self._load_neighbour_overlay.set_enabled(False) self._update_navigation_buttons() return True return False @listens(u'selected_index') def _on_focused_selection_changed(self): if self._delay_preview_list and not self.focused_item.is_loadable: self._preview_list_task.restart() else: self._replace_preview_list() self._update_navigation_buttons() self._prehear_selected_item() self._load_neighbour_overlay.set_enabled(False) self.notify_focused_item() def _get_actual_item(self, item): contained_item = getattr(item, u'contained_item', None) if contained_item is not None: return contained_item return item def _previous_can_be_loaded(self): return self.focused_list.selected_index > 0 and self.focused_list.items[self.focused_list.selected_index - 1].is_loadable def _next_can_be_loaded(self): items = self.focused_list.items return self.focused_list.selected_index < len(items) - 1 and items[self.focused_list.selected_index + 1].is_loadable @listens(u'load_next') def _on_load_next(self): self.focused_list.selected_index += 1 self._load_selected_item() @listens(u'load_previous') def _on_load_previous(self): self.focused_list.selected_index -= 1 self._load_selected_item() def _update_load_neighbour_overlay_visibility(self): self._load_neighbour_overlay.set_enabled(liveobj_valid(self._browser.hotswap_target) and (self._next_can_be_loaded() or self._previous_can_be_loaded()) and not self.focused_list.selected_item.is_device) def _load_selected_item(self): focused_list = self.focused_list self._update_load_neighbour_overlay_visibility() self._update_navigation_buttons() item = self._get_actual_item(focused_list.selected_item) self._load_item(item) self.notify_loaded() def _show_load_notification(self, item): notification_text = self._make_notification_text(item) text_length = len(notification_text) notification_time = self.MIN_TIME if text_length > self.MIN_TIME_TEXT_LENGTH: if text_length > self.MAX_TIME_TEXT_LENGTH: notification_time = self.MAX_TIME else: notification_time = self.MIN_TIME + (self.MAX_TIME - self.MIN_TIME) * old_div(float(text_length - self.MIN_TIME_TEXT_LENGTH), self.MAX_TIME_TEXT_LENGTH - self.MIN_TIME_TEXT_LENGTH) self.show_notification(notification_text, notification_time=notification_time) self._commit_model_changes() def _make_notification_text(self, browser_item): return u'Loading %s' % browser_item.name def _load_item(self, item): self._show_load_notification(item) if liveobj_valid(self._browser.hotswap_target): if isinstance(item, PluginPresetBrowserItem): self._browser.hotswap_target.selected_preset_index = item.preset_index else: self._browser.load_item(item) self._content_hotswap_target = self._browser.hotswap_target else: with self._insert_right_of_selected(): self._browser.load_item(item) @contextmanager def _insert_right_of_selected(self): DeviceInsertMode = Live.Track.DeviceInsertMode device_to_select = get_selection_for_new_device(self._selection) if device_to_select: self._selection.selected_object = device_to_select selected_track_view = self.song.view.selected_track.view selected_track_view.device_insert_mode = DeviceInsertMode.selected_right yield selected_track_view.device_insert_mode = DeviceInsertMode.default def _prehear_selected_item(self): if self.prehear_button.is_toggled and not self._updating_root_items: self._browser.stop_preview() item = self._get_actual_item(self.focused_list.selected_item) if item and item.is_loadable and isinstance(item, Live.Browser.BrowserItem): self._browser.preview_item(item) def _stop_prehear(self): if self.prehear_button.is_toggled and not self._updating_root_items: self._browser.stop_preview() def _update_navigation_buttons(self): focused_list = self.focused_list self.up_button.enabled = focused_list.selected_index > 0 self.down_button.enabled = focused_list.selected_index < len(focused_list.items) - 1 selected_item_loadable = self.focused_list.selected_item.is_loadable can_exit = self._focused_list_index > 0 assume_can_enter = self._preview_list_task.is_running and not selected_item_loadable can_enter = self._focused_list_index < len(self._lists) - 1 or assume_can_enter self.back_button.enabled = can_exit self.open_button.enabled = can_enter self.load_button.enabled = selected_item_loadable self._load_neighbour_overlay.can_load_previous = self._previous_can_be_loaded() self._load_neighbour_overlay.can_load_next = self._next_can_be_loaded() context_button_color = IndexedColor.from_live_index(self.context_color_index, DISPLAY_BUTTON_SHADE_LEVEL) if self.context_color_index > -1 else u'Browser.Navigation' self.load_button.color = context_button_color self.close_button.color = context_button_color self._load_neighbour_overlay.load_next_button.color = context_button_color self._load_neighbour_overlay.load_previous_button.color = context_button_color if not self._expanded: self.left_button.enabled = self.back_button.enabled self.right_button.enabled = can_enter or self._can_auto_expand() else: num_columns = int(ceil(old_div(float(len(self.focused_list.items)), self.NUM_ITEMS_PER_COLUMN))) last_column_start_index = (num_columns - 1) * self.NUM_ITEMS_PER_COLUMN self.left_button.enabled = self._focused_list_index > 0 self.right_button.enabled = can_enter or self.focused_list.selected_index < last_column_start_index self.can_enter = can_enter self.can_exit = can_exit def _update_scrolling(self): self.scrolling = self.up_button.is_pressed or self.down_button.is_pressed or self.scroll_focused_encoder.is_touched or any(map(lambda e: e.is_touched, self.scroll_encoders)) or self.right_button.is_pressed and self._expanded or self.left_button.is_pressed and self._expanded def _update_horizontal_navigation(self): self.horizontal_navigation = self.right_button.is_pressed or self.left_button.is_pressed def _update_context(self): selected_track = self.song.view.selected_track clip = self.song.view.detail_clip if self.browse_for_audio_clip and clip: self.context_text = clip.name elif liveobj_valid(self._browser.hotswap_target): self.context_text = self._browser.hotswap_target.name else: self.context_text = selected_track.name selected_track_color_index = selected_track.color_index self.context_color_index = selected_track_color_index if selected_track_color_index is not None else -1 def _enter_selected_item(self): item_entered = False self._finish_preview_list_task() new_index = self._focused_list_index + 1 if 0 <= new_index < len(self._lists): self._focus_list_with_index(new_index) self._unexpand_task.kill() self._update_list_offset() self._update_auto_expand() self._prehear_selected_item() item_entered = True return item_entered def _exit_selected_item(self): item_exited = False try: self._focus_list_with_index(self._focused_list_index - 1) self._update_list_offset() self._update_auto_expand() self._stop_prehear() item_exited = True except CannotFocusListError: pass return item_exited def _can_auto_expand(self): return len(self.focused_list.items) > self.NUM_ITEMS_PER_COLUMN * 2 and self.focused_list.selected_item.is_loadable and getattr(self.focused_list.selected_item, u'contained_item', None) == None def _update_auto_expand(self): self.expanded = self._can_auto_expand() self._update_list_offset() def _update_list_offset(self): if self.expanded: self.list_offset = max(0, self.focused_list_index - 1) else: offset = len(self._lists) if self.focused_list.selected_item.is_loadable: offset += 1 self.list_offset = max(0, offset - self.NUM_VISIBLE_BROWSER_LISTS) def _replace_preview_list_by_task(self): self._replace_preview_list() self._update_navigation_buttons() def _finish_preview_list_task(self): if self._preview_list_task.is_running: self._replace_preview_list_by_task() return True return False def _replace_preview_list(self): self._preview_list_task.kill() self._crop_browser_lists(self._focused_list_index + 1) selected_item = self.focused_list.selected_item children_iterator = selected_item.iter_children if len(children_iterator) > 0: enable_wrapping = getattr(selected_item, u'enable_wrapping', True) and self.focused_list.items_wrapped self._append_browser_list(children_iterator=children_iterator, limit=self.num_preview_items, enable_wrapping=enable_wrapping) def _append_browser_list(self, children_iterator, limit = -1, enable_wrapping = True): l = BrowserList(item_iterator=children_iterator, item_wrapper=self._wrap_item if enable_wrapping else nop, limit=limit) l.items_wrapped = enable_wrapping self._lists.append(l) self.register_disconnectable(l) self.notify_lists() def _crop_browser_lists(self, length): num_items_to_crop = len(self._lists) - length for _ in range(num_items_to_crop): l = self._lists.pop() self.unregister_disconnectable(l) if num_items_to_crop > 0: self.notify_lists() def _make_root_browser_items(self): filter_type = self._browser.filter_type hotswap_target = self._browser.hotswap_target if liveobj_valid(hotswap_target): filter_type = filter_type_for_hotswap_target(hotswap_target, default=filter_type) return make_root_browser_items(self._browser, filter_type) def _content_cache_is_valid(self): return self._content_filter_type == self._browser.filter_type and not liveobj_changed(self._content_hotswap_target, self._browser.hotswap_target) def _invalidate_content_cache(self): self._content_hotswap_target = None self._content_filter_type = None def _update_content_cache(self): self._content_filter_type = self._browser.filter_type self._content_hotswap_target = self._browser.hotswap_target def _update_root_items(self): if not self._content_cache_is_valid(): self._update_content_cache() with self._updating_root_items(): self._on_focused_selection_changed.subject = None self._crop_browser_lists(0) self._append_browser_list(children_iterator=self._make_root_browser_items()) self._focused_list_index = 0 self.focused_list.selected_index = 0 self._select_hotswap_target() self._on_focused_selection_changed.subject = self.focused_list self._on_focused_selection_changed() def _select_hotswap_target(self, list_index = 0): if list_index < len(self._lists): l = self._lists[list_index] l.access_all = True children = l.items i = index_if(lambda i: i.is_selected, children) if i < len(children): self._focused_list_index = list_index l.selected_index = i self._replace_preview_list() self._select_hotswap_target(list_index + 1) @property def num_preview_items(self): if self._expanded: return self.NUM_ITEMS_PER_COLUMN * self.NUM_COLUMNS_IN_EXPANDED_LIST return 6 def update(self): super(BrowserComponent, self).update() self._invalidate_content_cache() if self.is_enabled(): self._update_root_items() self._update_context() self._update_list_offset() self._update_load_neighbour_overlay_visibility() self._update_navigation_buttons() self.expanded = False self._update_list_offset() else: self._stop_prehear() self.list_offset = 0 def _wrap_item(self, item): if item.is_device: return self._wrap_device_item(item) if self._is_hotswap_target_plugin(item): return self._wrap_hotswapped_plugin_item(item) return item def _wrap_device_item(self, item): u""" Create virtual folder around items that can be loaded AND have children, to avoid having two actions on an item (open and load). """ wrapped_loadable = WrappedLoadableBrowserItem(name=item.name, is_loadable=True, contained_item=item) return FolderBrowserItem(name=item.name, is_loadable=True, is_device=True, contained_item=item, wrapped_loadable=wrapped_loadable, icon=u'browser_arrowcontent.svg') def _is_hotswap_target_plugin(self, item): return isinstance(self._browser.hotswap_target, Live.PluginDevice.PluginDevice) and isinstance(item, Live.Browser.BrowserItem) and self._browser.relation_to_hotswap_target(item) == Live.Browser.Relation.equal def _wrap_hotswapped_plugin_item(self, item): return PluginBrowserItem(name=item.name, vst_device=self._browser.hotswap_target)
class ClipPositions(EventObject): __events__ = ('is_recording', 'warp_markers', 'before_update_all', 'after_update_all') MAX_TIME = 10000000 MIN_TIME = -10000 start = listenable_property.managed(0.0) end = listenable_property.managed(0.0) start_marker = listenable_property.managed(0.0) end_marker = listenable_property.managed(0.0) loop_start = listenable_property.managed(0.0) loop_end = listenable_property.managed(0.0) loop_length = listenable_property.managed(0.0) use_beat_time = listenable_property.managed(False) def __init__(self, clip=None, *a, **k): (super(ClipPositions, self).__init__)(*a, **k) self._clip = clip self._looping = self._clip.looping self._ClipPositions__on_is_recording_changed.subject = clip self._ClipPositions__on_looping_changed.subject = clip self._ClipPositions__on_start_marker_changed.subject = clip self._ClipPositions__on_end_marker_changed.subject = clip self._ClipPositions__on_loop_start_changed.subject = clip self._ClipPositions__on_loop_end_changed.subject = clip self._ClipPositions__on_loop_start_changed() self._ClipPositions__on_loop_end_changed() if clip.is_audio_clip: self._ClipPositions__on_warping_changed.subject = clip self._ClipPositions__on_warp_markers_changed.subject = clip if clip.is_midi_clip: self._ClipPositions__on_notes_changed.subject = clip self._update_start_end_note_times() self.update_all() @property def is_warping(self): return self._clip.is_audio_clip and self._clip.warping def _convert_to_desired_unit(self, beat_time_or_seconds): if not self._clip.is_midi_clip: if not self.is_warping: beat_time_or_seconds = self._clip.seconds_to_sample_time(beat_time_or_seconds) return beat_time_or_seconds @listens('start_marker') def __on_start_marker_changed(self): if not self._process_looping_update(): self.start_marker = self._convert_to_desired_unit(self._clip.start_marker) @listens('end_marker') def __on_end_marker_changed(self): if not self._process_looping_update(): self.end_marker = self._convert_to_desired_unit(self._clip.end_marker) @listens('loop_start') def __on_loop_start_changed(self): if not self._process_looping_update(): self.loop_start = self._convert_to_desired_unit(self._clip.loop_start) self._update_loop_length() @listens('loop_end') def __on_loop_end_changed(self): if not self._process_looping_update(): self.loop_end = self._convert_to_desired_unit(self._clip.loop_end) self._update_loop_length() @listens('is_recording') def __on_is_recording_changed(self): self._update_start_end() self.notify_is_recording() @listens('warp_markers') def __on_warp_markers_changed(self): self.update_all() self.notify_warp_markers() @listens('looping') def __on_looping_changed(self): self.update_all() @listens('warping') def __on_warping_changed(self): self.update_all() @listens('notes') def __on_notes_changed(self): self._update_start_end_note_times() self._update_start_end() def _update_start_end_note_times(self): all_notes = self._clip.get_notes_extended(from_time=(self.MIN_TIME), from_pitch=0, time_span=(self.MAX_TIME), pitch_span=128) start_times, end_times = list(zip(*[(note.start_time, note.start_time + note.duration) for note in all_notes])) if len(all_notes) > 0 else ( [ self.MAX_TIME], [self.MIN_TIME]) self.start_of_first_note = min(start_times) self.end_of_last_note = max(end_times) def _process_looping_update(self): looping = self._clip.looping if looping != self._looping: self._looping = looping self.update_all() return True return False def _update_loop_length(self): self.loop_length = self._convert_to_desired_unit(self._clip.loop_end) - self._convert_to_desired_unit(self._clip.loop_start) def _update_start_end(self): start = None end = None if self.is_warping: start = self._clip.sample_to_beat_time(0) end = self._clip.sample_to_beat_time(self._clip.sample_length) elif self._clip.is_audio_clip: start = 0 end = self._clip.sample_length else: start = self.start_of_first_note end = self.end_of_last_note self.start = min(start, self.loop_start if self._clip.looping else self.start_marker) self.end = max(end, self.loop_end) def update_all(self): self.notify_before_update_all() self._ClipPositions__on_start_marker_changed() self._ClipPositions__on_end_marker_changed() self._ClipPositions__on_loop_start_changed() self._ClipPositions__on_loop_end_changed() self._update_start_end() if self._clip.is_audio_clip: self.use_beat_time = self._clip.warping self.notify_after_update_all()
class ScalesComponent(Component): navigation_colors = dict(color=b'Scales.Navigation', disabled_color=b'Scales.NavigationDisabled') up_button = ButtonControl(repeat=True, **navigation_colors) down_button = ButtonControl(repeat=True, **navigation_colors) right_button = ButtonControl(repeat=True, **navigation_colors) left_button = ButtonControl(repeat=True, **navigation_colors) root_note_buttons = control_list(RadioButtonControl, control_count=len(ROOT_NOTES), checked_color=b'Scales.OptionOn', unchecked_color=b'Scales.OptionOff') in_key_toggle_button = ToggleButtonControl( toggled_color=b'Scales.OptionOn', untoggled_color=b'Scales.OptionOn') fixed_toggle_button = ToggleButtonControl( toggled_color=b'Scales.OptionOn', untoggled_color=b'Scales.OptionOff') scale_encoders = control_list(StepEncoderControl) layout_encoder = StepEncoderControl() direction_encoder = StepEncoderControl() horizontal_navigation = listenable_property.managed(False) NUM_DISPLAY_ROWS = 4 NUM_DISPLAY_COLUMNS = int(ceil(float(len(SCALES)) / NUM_DISPLAY_ROWS)) def __init__(self, note_layout=None, *a, **k): assert note_layout is not None super(ScalesComponent, self).__init__(*a, **k) self._note_layout = note_layout self._scale_list = list(SCALES) self._scale_name_list = map(lambda m: m.name, self._scale_list) self._selected_scale_index = -1 self._selected_root_note_index = -1 self._layouts = (Layout(b'4ths', 3), Layout(b'3rds', 2), Layout(b'Sequential', None)) self._selected_layout_index = 0 self.in_key_toggle_button.connect_property(note_layout, b'is_in_key') self.fixed_toggle_button.connect_property(note_layout, b'is_fixed') self.__on_root_note_changed.subject = self._note_layout self.__on_scale_changed.subject = self._note_layout self.__on_interval_changed.subject = self._note_layout self.__on_root_note_changed(note_layout.root_note) self.__on_scale_changed(note_layout.scale) self.__on_interval_changed(self._note_layout.interval) return def _set_selected_scale_index(self, index): index = clamp(index, 0, len(self._scale_list) - 1) self._note_layout.scale = self._scale_list[index] @down_button.pressed def down_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + 1) @up_button.pressed def up_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index - 1) @left_button.pressed def left_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index - self.NUM_DISPLAY_ROWS) @right_button.pressed def right_button(self, button): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + self.NUM_DISPLAY_ROWS) @root_note_buttons.pressed def root_note_buttons(self, button): self._note_layout.root_note = ROOT_NOTES[button.index] @listens(b'root_note') def __on_root_note_changed(self, root_note): self._selected_root_note_index = list(ROOT_NOTES).index(root_note) self.root_note_buttons.checked_index = self._selected_root_note_index self.notify_selected_root_note_index() @property def root_note_names(self): return [NOTE_NAMES[note] for note in ROOT_NOTES] @listenable_property def selected_root_note_index(self): return self._selected_root_note_index @scale_encoders.value def scale_encoders(self, value, encoder): self._update_horizontal_navigation() self._set_selected_scale_index(self._selected_scale_index + value) @property def scale_names(self): return self._scale_name_list @listenable_property def selected_scale_index(self): return self._selected_scale_index @listens(b'scale') def __on_scale_changed(self, scale): index = self._scale_list.index( scale) if scale in self._scale_list else -1 if index != self._selected_scale_index: self._selected_scale_index = index self.up_button.enabled = index > 0 self.left_button.enabled = index > 0 self.down_button.enabled = index < len(self._scale_list) - 1 self.right_button.enabled = index < len(self._scale_list) - 1 self.notify_selected_scale_index() @layout_encoder.value def layout_encoder(self, value, encoder): index = clamp(self._selected_layout_index + value, 0, len(self._layouts) - 1) self.selected_layout_index = index @property def layout_names(self): return [layout.name for layout in self._layouts] @listenable_property def selected_layout_index(self): return self._selected_layout_index @selected_layout_index.setter def selected_layout_index(self, index): if index != self._selected_layout_index: self._selected_layout_index = index interval = self._layouts[index].interval self._note_layout.interval = interval self.notify_selected_layout_index() @direction_encoder.value def direction_encoder(self, value, encoder): self._note_layout.is_horizontal = value < 0 @property def note_layout(self): return self._note_layout def _update_horizontal_navigation(self): self.horizontal_navigation = self.right_button.is_pressed or self.left_button.is_pressed @listens(b'interval') def __on_interval_changed(self, interval): index = index_if(lambda layout: layout.interval == interval, self._layouts) self.selected_layout_index = index
class WaveformNavigation(EventObject): """ Class for managing a visible area of a waveform """ visible_region = listenable_property.managed(Region(0, 1)) visible_region_in_samples = listenable_property.managed(Region(0, 1)) animate_visible_region = listenable_property.managed(False) focus_marker = listenable_property.managed(FocusMarker('', 0)) show_focus = listenable_property.managed(False) ZOOM_SENSITIVITY = 1.5 MIN_VISIBLE_SAMPLES = 49 WAVEFORM_WIDTH_IN_PX = 933 MARGIN_IN_PX = 121 RELATIVE_FOCUS_MARGIN = float(MARGIN_IN_PX) / WAVEFORM_WIDTH_IN_PX UNSNAPPING_THRESHOLD = 0.6 CHANGE_OBJECT_TIME = 0.1 def __init__(self, *a, **k): super(WaveformNavigation, self).__init__(*a, **k) self._waveform_region = Region(0, 1) self.waveform_roi = self.make_region_of_interest( getter=lambda: self._waveform_region, with_margin=False) self.focused_object_roi = self.make_region_of_interest( getter=self._make_region_for_focused_object, with_margin=False) self._focused_identifier = None self._touched_identifiers = set() self._changed_identifiers = set() self._has_tasks = False self._target_roi = self.waveform_roi self._source_roi = self.waveform_roi self._request_select_region = False self._unsnapping_value = 0 self._locked_roi = None self._last_action = None return def disconnect(self): super(WaveformNavigation, self).disconnect() if self._has_tasks: self._tasks.kill() self._tasks.clear() def get_object_identifier(self, obj): raise NotImplementedError def get_zoom_object(self): raise NotImplementedError def get_region_in_samples(self, region): return region def get_min_visible_length(self): return self.MIN_VISIBLE_SAMPLES @listenable_property def waveform_region(self): return self._waveform_region @waveform_region.setter def waveform_region(self, region): if region != self._waveform_region: self._waveform_region = region self._request_select_region = True self.set_visible_region(self._waveform_region) self.notify_waveform_region() def make_region_of_interest(self, start_identifier=None, end_identifier=None, getter=None, with_margin=True): return RegionOfInterest( start_identifier, end_identifier, getter, add_margin=self._add_margin_to_region if with_margin else nop) @lazy_attribute def regions_of_interest(self): """ The region of interests, that can be zoomed into. By default, the waveform navigation zooms only between the full waveform and the focused objects position. Override additional_regions_of_interest to add more regions to it. """ rois = { 'waveform': self.waveform_roi, 'focused_object': self.focused_object_roi } rois.update(self.additional_regions_of_interest) return rois @lazy_attribute def additional_regions_of_interest(self): return {} def get_name_for_roi(self, roi): """ Returns the name for the given roi or None, if it doesn't have one """ item = find_if(lambda i: i[1] == roi, self.regions_of_interest.iteritems()) if item is not None: return item[0] else: return @lazy_attribute def focusable_object_descriptions(self): """ Describes focusable objects and how they zoom into regions. Returns a dictionary of identifier to ObjectDescriptions. """ return {} def get_object_description(self, identifier): return self.focusable_object_descriptions.get(identifier, None) @property def visible_proportion(self): """ Returns the proportion between the visible length and the sample length """ return self.visible_region.length / float(self._waveform_region.length) def set_visible_region(self, region, source_action=None, force_animate=False): """ Set the current visible region in the current unit and samples. The region is animated, if source action changes. Animation is enforced, if force_animate is True. """ self.animate_visible_region = force_animate or source_action != self._last_action self.visible_region = region.clamp_to_region(self._waveform_region) self.visible_region_in_samples = self.get_region_in_samples( self.visible_region) self._last_action = source_action def set_visible_length(self, length): """ Extends or reduces the visible end to show the given length. If the end of the waveform is reached, the visible start is adapted. """ start = self.visible_region.start end = min(start + length, self.waveform_region.end) start = end - length self.set_visible_region(Region(start, end)) def zoom(self, value): """ Zooms in or out of the waveform start value should be between -1.0 and 1.0, where 1.0 will zoom in as much as possible and -1.0 will zoom out completely. """ animate = self._request_select_region if self._request_select_region or self._process_unsnapping(value): self._select_region(value > 0) source = self._source_roi.region_with_margin target = self._target_roi.region_with_margin easing_degree = calc_easing_degree_for_proportion( float(target.length) / float(source.length)) focused_region, focus_marker, margin_type = self._get_zoom_info_for_focused_object( ) t = inverse_interpolate_region( source, target, self.visible_region, easing_degree, prefer_end=margin_type == MarginType.START) t = clamp(t + value * self.ZOOM_SENSITIVITY, 0.0, 1.0) region = interpolate_region(source, target, t, easing_degree) region = self._add_margin_to_zoomed_region(region, focused_region, margin_type) self.set_visible_region(region, force_animate=animate, source_action='zoom') self.focus_marker = focus_marker self.show_focus = True self.try_hide_focus_delayed() self._try_lock_region() def _get_zoom_info_for_focused_object(self): """ Returns a tuple of the region for the focused object and weather a margin should be added to the zoom region. """ identifier = self._focused_identifier roi = self._get_roi_for_object_identifier(identifier) margin_type = MarginType.NONE region = None focus_marker = None if roi is not None: margin = self.waveform_region.length * self.RELATIVE_FOCUS_MARGIN region = roi.region is_start = roi.start_identifier == identifier if is_start and region.start < margin: margin_type = MarginType.START elif not is_start and region.end > self.waveform_region.end - margin: margin_type = MarginType.END obj_description = self.focusable_object_descriptions.get( identifier, None) if obj_description is not None: focus_marker = FocusMarker( obj_description.focus_name, region.end if roi.end_identifier == identifier else region.start) return (region, focus_marker, margin_type) def _add_margin_to_zoomed_region(self, zoom_region, focused_region, margin_type): """ Adds a margin to a zoom region, so that the focused object is shown with a margin as soon as possible. This makes switching between zooming an focusing an object seamless, as focusing will always add the margin as well. """ if focused_region is not None and margin_type != MarginType.NONE: position = focused_region.start if margin_type == MarginType.START else focused_region.end if zoom_region.start <= position <= zoom_region.end: if margin_type == MarginType.START: zoom_region = self._add_margin_to_zoomed_region_start( zoom_region, position) else: zoom_region = self._add_margin_to_zoomed_region_end( zoom_region, position) else: logger.warn( "Focused object not visible. Couldn't add margin to zoomed region. %d not in %r" % (position, zoom_region)) return zoom_region def _add_margin_to_zoomed_region_start(self, region, focused_position): p = focused_position - self.waveform_region.start samples_per_pixel = p / self.MARGIN_IN_PX length = self.WAVEFORM_WIDTH_IN_PX * samples_per_pixel if self.waveform_region.start + length < region.end: region = Region(self.waveform_region.start, region.end) else: p = region.end - focused_position samples_per_pixel = p / (self.WAVEFORM_WIDTH_IN_PX - self.MARGIN_IN_PX) length = self.WAVEFORM_WIDTH_IN_PX * samples_per_pixel start = region.end - length if start < region.start: region = Region(start, region.end) return region def _add_margin_to_zoomed_region_end(self, region, focused_position): p = self.waveform_region.end - focused_position samples_per_pixel = p / self.MARGIN_IN_PX length = self.WAVEFORM_WIDTH_IN_PX * samples_per_pixel if self.waveform_region.end - length > region.start: region = Region(region.start, self.waveform_region.end) else: p = focused_position - region.start samples_per_pixel = p / (self.WAVEFORM_WIDTH_IN_PX - self.MARGIN_IN_PX) length = self.WAVEFORM_WIDTH_IN_PX * samples_per_pixel end = region.start + length if end > region.end: region = Region(region.start, end) return region def _process_unsnapping(self, value): """ Process unsnapping for the given normalized value. Returns true if the region should be unsnapped. """ if self.is_snapped: self._unsnapping_value += value return abs(self._unsnapping_value) >= self.UNSNAPPING_THRESHOLD return False def _try_lock_region(self): if self.visible_region == self._waveform_region: self._locked_roi = None elif self.visible_region == self._target_roi.region_with_margin: self._locked_roi = self._target_roi elif self.visible_region == self._source_roi.region_with_margin: self._locked_roi = self._source_roi else: self._locked_roi = None return @property def is_snapped(self): return self.visible_region == self._target_roi.region_with_margin or self.visible_region == self._source_roi.region_with_margin def focus_object(self, obj): if obj != self.get_zoom_object(): identifier = self.get_object_identifier(obj) zoom_identifier = self.get_object_identifier( self.get_zoom_object()) touched_identifiers = self._touched_identifiers - set( [zoom_identifier]) objects_to_show = self._changed_identifiers & touched_identifiers if identifier in self.focusable_object_descriptions: if len(objects_to_show) > 1: logger.debug('Focus all objects %r' % objects_to_show) self._focused_identifier = identifier self._show_all_objects(objects_to_show) else: logger.debug('Focus object %r' % identifier) animate = len( touched_identifiers) <= 1 and self.object_changed( self._focused_identifier, identifier) self._focused_identifier = identifier self._focus_object_by_identifier(identifier, animate=animate) return True return False def object_changed(self, identifier1, identifier2): return identifier1 != identifier2 def _get_roi_for_object_identifier(self, identifier): return find_if(lambda roi: roi.bound_by(identifier), self.regions_of_interest.values()) def _get_position_for_identifier(self, identifier): roi = self._get_roi_for_object_identifier(identifier) if roi is not None: if roi.start_identifier == identifier: return roi.region.start return roi.region.end else: return def _zoom_out_or_move_region(self, source_region, target_region): """ Zooms out the source region, if it's contained in the target region or moves it left or right depending on where they overlap. """ new_region = None if source_region.inside(target_region): new_region = target_region elif target_region.start < source_region.start: new_region = Region( target_region.start, max(target_region.start + source_region.length, target_region.end)) elif target_region.end > source_region.end: new_region = Region( min(target_region.end - source_region.length, target_region.start), target_region.end) return new_region def _show_all_objects(self, identifiers): start = self.waveform_region.end end = self.waveform_region.start positions = imap(self._get_position_for_identifier, identifiers) for position in ifilter(None, positions): start = min(start, position) end = max(end, position) margin = self.visible_region.length * self.RELATIVE_FOCUS_MARGIN visible_region_without_margin = Region( self.visible_region.start + margin, self.visible_region.end - margin) object_region = Region(start, end) new_region = self._zoom_out_or_move_region( visible_region_without_margin, object_region) if new_region is not None: self.set_visible_region(self._add_margin_to_region(new_region), source_action='show_objects %r' % identifiers) self._request_select_region = True self._locked_roi = None self.focus_marker = FocusMarker('', 0) return def _focus_object_by_identifier(self, identifier, animate=False): """ Focuses the object in the waveform and brings it into the visible range. The visible length is preserved. The position is aligned to the left or right of the visible range, with a certain margin defined by RELATIVE_FOCUS_MARGIN. If the objects region boundary is already in the visible range, the visible position is not changing. :identifier: the object identifier to focus :animate: should be set to True if, if it should animate to the new position """ roi = self._get_roi_for_object_identifier(identifier) region = roi.region if self._locked_roi is not None and self._locked_roi.bound_by( identifier): if region.start < self.waveform_region.start: start = self.waveform_region.start new_visible_region = Region(start, start + self.visible_region.length) elif region.end > self.waveform_region.end: end = self.waveform_region.end new_visible_region = Region(end - self.visible_region.length, end) else: new_visible_region = self._add_margin_to_region(region) self.set_visible_region(new_visible_region, force_animate=animate) else: visible_length = self.visible_region.length visible_margin = visible_length * self.RELATIVE_FOCUS_MARGIN waveform_start, waveform_end = self._waveform_region if roi.end_identifier == identifier: start = min(region.start - visible_margin, self.visible_region.start) right = max(region.end + visible_margin, start + visible_length) left = right - visible_length else: end = max(region.end + visible_margin, self.visible_region.end) left = min(region.start - visible_margin, end - visible_length) right = left + visible_length self.set_visible_region(Region( clamp(left, waveform_start, waveform_end - visible_length), clamp(right, waveform_start + visible_length, waveform_end)), force_animate=animate) self._request_select_region = True self.focus_marker = FocusMarker( self.focusable_object_descriptions[identifier].focus_name, region.end if roi.end_identifier == identifier else region.start) return def touch_object(self, obj): is_zoom_object = obj == self.get_zoom_object() if is_zoom_object and self.is_snapped: self._request_select_region = True self._touched_identifiers.add(self.get_object_identifier(obj)) if self.focus_object(obj) or is_zoom_object: self.show_focus = True def release_object(self, obj): identifier = self.get_object_identifier(obj) self._remove_changed_object(identifier) if identifier in self._touched_identifiers: self._touched_identifiers.remove(identifier) self.try_hide_focus() def _remove_changed_object(self, identifier): if identifier in self._changed_identifiers: self._changed_identifiers.remove(identifier) def _remove_changed_object_delayed(self, identifier): tasks = self._tasks if tasks is not None: tasks.add( task.sequence( task.wait(self.CHANGE_OBJECT_TIME), task.run(partial(self._remove_changed_object, identifier)))) return def change_object(self, obj): identifier = self.get_object_identifier(obj) self._changed_identifiers.add(identifier) self._remove_changed_object_delayed(identifier) if self.focus_object(obj) or obj == self.get_zoom_object(): self.show_focus = True self.try_hide_focus_delayed() def focus_region_of_interest(self, roi_identifier, focused_object): roi = self.regions_of_interest[roi_identifier] visible_region = roi.region_with_margin self.set_visible_region(visible_region) self.focus_object(focused_object) if visible_region != self._waveform_region: self._locked_roi = roi def try_hide_focus(self): """ Hides the focus, if the focused object is not longer touched """ if self._should_hide_focus(): self.show_focus = False def try_hide_focus_delayed(self): """ Hides the focus after some time, if the focused object is not longer touched """ if self._hide_focus_task and self._should_hide_focus(): self._hide_focus_task.restart() def _should_hide_focus(self): zoom_identifier = self.get_object_identifier(self.get_zoom_object()) return zoom_identifier not in self._touched_identifiers and self._focused_identifier not in self._touched_identifiers def reset_focus_and_animation(self): self.show_focus = False self.animate_visible_region = False self._touched_identifiers = set() self._changed_identifiers = set() def copy_state(self, navigation): """ Tries to replicate the state of the given waveform navigation. The waveform regions need to be identical for this to make sense. The focused identifier and all region of interests should be available in both navigations, or the result will be undefined. """ if self._waveform_region == navigation.waveform_region: self.set_visible_region(navigation.visible_region) self._focused_identifier = navigation._focused_identifier source_roi_name = navigation.get_name_for_roi( navigation._source_roi) target_roi_name = navigation.get_name_for_roi( navigation._target_roi) locked_roi_name = navigation.get_name_for_roi( navigation._locked_roi) self._source_roi = self.regions_of_interest.get( source_roi_name, None) self._target_roi = self.regions_of_interest.get( target_roi_name, None) self._locked_roi = self.regions_of_interest.get( locked_roi_name, None) return @lazy_attribute @depends(parent_task_group=const(None)) def _tasks(self, parent_task_group=None): if parent_task_group is not None: tasks = parent_task_group.add(task.TaskGroup()) self._has_tasks = True return tasks else: return @lazy_attribute def _hide_focus_task(self): tasks = self._tasks if tasks is not None: return tasks.add( task.sequence(task.wait(EncoderControl.TOUCH_TIME), task.run(self.try_hide_focus))) else: return def _add_margin_to_region(self, region): start, end = region margin = self.RELATIVE_FOCUS_MARGIN start1 = (margin * start + end * margin - start) / (2 * margin - 1) start1 = self._waveform_region.clamp_position(start1) end1 = (end - margin * start1) / (1 - margin) end2 = (margin * start + end * margin - end) / (2 * margin - 1) end2 = self._waveform_region.clamp_position(end2) start2 = (start - margin * end2) / (1 - margin) return Region(max(start1, start2), min(end1, end2)) def _make_region_from_position_identifier(self, identifier): roi = self._get_roi_for_object_identifier(identifier) align_right = roi.end_identifier == identifier region = roi.region position = region.end if align_right else region.start length = self.get_min_visible_length() margin = self.RELATIVE_FOCUS_MARGIN * length if align_right: right = self._waveform_region.clamp_position(position + margin) left = self._waveform_region.clamp_position(right - length) else: left = self._waveform_region.clamp_position(position - margin) right = self._waveform_region.clamp_position(left + length) return Region(left, right) def _make_region_for_focused_object(self): if self._focused_identifier is not None: return self._make_region_from_position_identifier( self._focused_identifier) else: return Region(0, 0) def _get_roi_for_focused_identifier(self): if self._focused_identifier is not None: return map( self.regions_of_interest.get, self.get_object_description(self._focused_identifier).regions) else: return [] def _get_unique_regions_of_interest(self): """ Eliminates duplicates of the current regions and returns the remaining getters sorted by the length of the regions. """ rois = OrderedDict() for roi in self._get_roi_for_focused_identifier(): rois[roi.region_with_margin] = roi items = sorted(rois.items(), key=lambda (r, _): r.length, reverse=True) return map(lambda item: item[1], items) def _select_region_around_visible_region(self): regions_of_interest = self._get_unique_regions_of_interest() source_roi = find_if( lambda roi: self.visible_region.inside(roi.region_with_margin), reversed(regions_of_interest[:-1])) if source_roi is not None: self._set_source_and_target_roi( source_roi, regions_of_interest[regions_of_interest.index(source_roi) + 1]) return def _select_reached_region(self, zoom_in): rois = self._get_unique_regions_of_interest() i = index_if(lambda roi: self.visible_region == roi.region_with_margin, rois) if i != len(rois): if zoom_in: if i < len(rois) - 1: self._set_source_and_target_roi(rois[i], rois[i + 1]) elif i > 0: self._set_source_and_target_roi(rois[i - 1], rois[i]) return True return False def _select_region(self, zoom_in): if not self._select_reached_region(zoom_in): self._select_region_around_visible_region() self._request_select_region = False self._unsnapping_value = 0 def _set_source_and_target_roi(self, source_roi, target_roi): self._source_roi = source_roi self._target_roi = target_roi if logger.isEnabledFor(logging.DEBUG): self._report_current_source_and_target_roi() def _report_current_source_and_target_roi(self): source_roi_name = '' target_roi_name = '' for name, roi in self.regions_of_interest.iteritems(): if roi == self._source_roi: source_roi_name = name elif roi == self._target_roi: target_roi_name = name logger.debug('Zooming between roi "%s" and "%s"' % (source_roi_name, target_roi_name))
class LoopSelectorComponent(Component, Messenger): u""" Component that uses a button matrix to display the timeline of a clip. It allows you to select the loop of the clip and a page within it of a given Paginator object. """ next_page_button = ButtonControl() prev_page_button = ButtonControl() delete_button = ButtonControl() select_button = ButtonControl() loop_selector_matrix = control_matrix(PadControl, sensitivity_profile=u'loop', mode=PlayableControl.Mode.listenable) short_loop_selector_matrix = control_matrix(ButtonControl) is_following = listenable_property.managed(False) def __init__(self, clip_creator = None, measure_length = 4.0, follow_detail_clip = False, paginator = None, default_size = None, *a, **k): super(LoopSelectorComponent, self).__init__(*a, **k) assert default_size is not None self._clip_creator = clip_creator self._sequencer_clip = None self._paginator = Paginator() self._loop_start = 0 self._loop_end = 0 self._loop_length = 0 self._default_size = default_size self._pressed_pages = [] self._page_colors = [] self._measure_length = measure_length self._last_playhead_page = -1 def set_is_following_true(): self.is_following = True self._follow_task = self._tasks.add(task.sequence(task.wait(defaults.MOMENTARY_DELAY), task.run(set_is_following_true))) self._follow_task.kill() self.set_step_duplicator(None) self._notification_reference = partial(nop, None) self.is_deleting = False if follow_detail_clip: self._on_detail_clip_changed.subject = self.song.view self._on_detail_clip_changed() self._on_session_record_changed.subject = self.song self._on_song_playback_status_changed.subject = self.song if paginator is not None: self.set_paginator(paginator) return def set_paginator(self, paginator): self._paginator = paginator or Paginator() self._on_page_index_changed.subject = paginator self._on_page_length_changed.subject = paginator self._update_page_colors() @listens(u'page_index') def _on_page_index_changed(self): self._update_page_colors() @listens(u'page_length') def _on_page_length_changed(self): self._update_page_colors() self._select_start_page() def set_step_duplicator(self, duplicator): self._step_duplicator = duplicator or NullStepDuplicator() self._step_duplicator.set_clip(self._sequencer_clip) @listens(u'detail_clip') def _on_detail_clip_changed(self): self.set_detail_clip(self.song.view.detail_clip) def set_detail_clip(self, clip): if liveobj_changed(clip, self._sequencer_clip): self.is_following = liveobj_valid(clip) and (self.is_following or clip_is_new_recording(clip)) self._on_playing_position_changed.subject = clip self._on_playing_status_changed.subject = clip self._on_loop_start_changed.subject = clip self._on_loop_end_changed.subject = clip self._on_is_recording_changed.subject = clip self._sequencer_clip = clip self._step_duplicator.set_clip(clip) self._on_loop_changed() def _select_start_page(self): if liveobj_valid(self._sequencer_clip): page_start = self._paginator.page_index * self._paginator.page_length to_select = page_start if page_start <= self._sequencer_clip.loop_start: to_select = self._sequencer_clip.loop_start elif page_start >= self._sequencer_clip.loop_end: to_select = max(self._sequencer_clip.loop_end - self._paginator.page_length, self._sequencer_clip.loop_start) self._paginator.select_page_in_point(to_select) @listens(u'loop_start') def _on_loop_start_changed(self): self._on_loop_changed() @listens(u'loop_end') def _on_loop_end_changed(self): self._on_loop_changed() def _on_loop_changed(self): if liveobj_valid(self._sequencer_clip): self._loop_start = self._sequencer_clip.loop_start self._loop_end = self._sequencer_clip.loop_end self._loop_length = self._loop_end - self._loop_start else: self._loop_start = 0 self._loop_end = 0 self._loop_length = 0 self._select_start_page() self._update_page_colors() def set_loop_selector_matrix(self, matrix): self.loop_selector_matrix.set_control_element(matrix) self._update_page_colors() def set_short_loop_selector_matrix(self, matrix): self.short_loop_selector_matrix.set_control_element(matrix) self._update_page_colors() def update(self): super(LoopSelectorComponent, self).update() self._update_page_and_playhead_leds() @listens(u'is_recording') def _on_is_recording_changed(self): self.is_following = self.is_following or clip_is_new_recording(self._sequencer_clip) @listens(u'playing_position') def _on_playing_position_changed(self): self._update_page_and_playhead_leds() self._update_page_selection() @listens(u'playing_status') def _on_playing_status_changed(self): self._update_page_and_playhead_leds() @listens(u'session_record') def _on_session_record_changed(self): self._update_page_and_playhead_leds() @listens(u'is_playing') def _on_song_playback_status_changed(self): self._update_page_and_playhead_leds() def _has_running_clip(self): return liveobj_valid(self._sequencer_clip) and (self._sequencer_clip.is_playing or self._sequencer_clip.is_recording) def _update_page_selection(self): if self.is_enabled() and self.is_following and self._has_running_clip(): position = self._sequencer_clip.playing_position self._paginator.select_page_in_point(position) def _update_page_and_playhead_leds(self): @contextmanager def save_page_color(page_colors, page): old_page_value = page_colors[page] yield page_colors[page] = old_page_value @contextmanager def replace_and_restore_tail_colors(page_colors, page): if clip_is_new_recording(self._sequencer_clip): old_tail_values = page_colors[page + 1:] page_colors[page + 1:] = [u'LoopSelector.OutsideLoop'] * len(old_tail_values) yield if clip_is_new_recording(self._sequencer_clip): page_colors[page + 1:] = old_tail_values if self.is_enabled() and self._has_running_clip(): position = self._sequencer_clip.playing_position visible_page = int(position / self._page_length_in_beats) - self.page_offset page_colors = self._page_colors if 0 <= visible_page < len(page_colors): with save_page_color(page_colors, visible_page): if self.song.is_playing: page_colors[visible_page] = u'LoopSelector.PlayheadRecord' if self.song.session_record else u'LoopSelector.Playhead' with replace_and_restore_tail_colors(page_colors, visible_page): self._update_page_leds() else: self._update_page_leds() self._last_playhead_page = visible_page else: self._update_page_leds() def _get_size(self): return max(self.loop_selector_matrix.control_count, self.short_loop_selector_matrix.control_count, self._default_size) def _get_loop_in_pages(self): page_length = self._page_length_in_beats loop_start = int(self._loop_start / page_length) loop_end = int(self._loop_end / page_length) loop_length = loop_end - loop_start + int(self._loop_end % page_length != 0) return (loop_start, loop_length) def _selected_pages_range(self): size = self._get_size() page_length = self._page_length_in_beats seq_page_length = max(self._paginator.page_length / page_length, 1) seq_page_start = int(self._paginator.page_index * self._paginator.page_length / page_length) seq_page_end = int(min(seq_page_start + seq_page_length, self.page_offset + size)) return (seq_page_start, seq_page_end) def _update_page_colors(self): u""" Update the offline array mapping the timeline of the clip to buttons. """ page_length = self._page_length_in_beats size = self._get_size() def calculate_page_colors(): l_start, l_length = self._get_loop_in_pages() page_offset = self.page_offset pages_per_measure = int(self._one_measure_in_beats / page_length) def color_for_page(absolute_page): if l_start <= absolute_page < l_start + l_length: if absolute_page % pages_per_measure == 0: return u'LoopSelector.InsideLoopStartBar' return u'LoopSelector.InsideLoop' else: return u'LoopSelector.OutsideLoop' return map(color_for_page, xrange(page_offset, page_offset + size)) def mark_selected_pages(page_colors): for page_index in xrange(*self._selected_pages_range()): button_index = page_index - self.page_offset if page_colors[button_index].startswith(u'LoopSelector.InsideLoop'): page_colors[button_index] = u'LoopSelector.SelectedPage' page_colors = calculate_page_colors() mark_selected_pages(page_colors) self._page_colors = page_colors self._update_page_and_playhead_leds() def _update_page_leds(self): self._update_page_leds_in_matrix(self.loop_selector_matrix) self._update_page_leds_in_matrix(self.short_loop_selector_matrix) def _update_page_leds_in_matrix(self, matrix): u""" update hardware leds to match precomputed map """ if self.is_enabled() and matrix: for button, color in izip(matrix, self._page_colors): button.color = color def _jump_to_page(self, next_page): start, length = self._get_loop_in_pages() if next_page >= start + length: next_page = start elif next_page < start: next_page = start + length - 1 self._paginator.select_page_in_point(next_page * self._page_length_in_beats) @next_page_button.pressed def next_page_button(self, button): if self.is_following: self.is_following = False else: _, end = self._selected_pages_range() self._jump_to_page(end) self._follow_task.restart() @next_page_button.released def next_page_button(self, button): self._follow_task.kill() @prev_page_button.pressed def prev_page_button(self, button): if self.is_following: self.is_following = False else: begin, end = self._selected_pages_range() self._jump_to_page(begin - (end - begin)) self._follow_task.restart() @prev_page_button.released def prev_page_button(self, button): self._follow_task.kill() @short_loop_selector_matrix.pressed def short_loop_selector_matrix(self, button): if self.is_enabled(): page = self._get_corresponding_page(button, self.short_loop_selector_matrix) self._pressed_pages = [page] self._try_set_loop() self._pressed_pages = [] @loop_selector_matrix.pressed def loop_selector_matrix(self, button): if self.is_enabled(): page = self._get_corresponding_page(button, self.loop_selector_matrix) if page not in self._pressed_pages: self._on_press_loop_selector_matrix(page) @loop_selector_matrix.released def loop_selector_matrix(self, button): page = self._get_corresponding_page(button, self.loop_selector_matrix) if page in self._pressed_pages: self._pressed_pages.remove(page) def _get_corresponding_page(self, button, matrix): y, x = button.coordinate return x + y * matrix.width def _quantize_page_index(self, page_index, quant): page_length = self._page_length_in_beats return quant * float(int(page_length * page_index / quant)) def _clear_page(self, page): page_start, page_end = self._selected_pages_time_range(page) notes = self._sequencer_clip.get_notes(page_start, 0, page_end, 128) if len(notes) > 0: self._sequencer_clip.remove_notes(page_start, 0, page_end - page_start, 128) self._notification_reference = self.show_notification(MessageBoxText.PAGE_CLEARED) else: self._notification_reference = self.show_notification(MessageBoxText.CANNOT_CLEAR_EMPTY_PAGE) def _selected_pages_time_range(self, page): page_start = 0 page_end = 0 page_length = self._page_length_in_beats if self._loop_length > page_length: range_start, range_end = self._selected_pages_range() page_start = range_start * page_length page_end = range_end * page_length else: page_start = page * page_length page_end = page_start + page_length return (page_start, page_end) def _add_page_to_duplicator(self, page): page_start, page_end = self._selected_pages_time_range(page) self._step_duplicator.add_step(page_start, page_end, nudge_offset=0, is_page=True) def _on_press_loop_selector_matrix(self, page): def create_clip(pages): measure = self._one_measure_in_beats length = self._quantize_page_index(pages, measure) + measure create_clip_in_selected_slot(self._clip_creator, self.song, length) def handle_page_press_on_clip(page): l_start, l_length = self._get_loop_in_pages() page_in_loop = l_start <= page < l_start + l_length buttons_pressed = len(self._pressed_pages) if buttons_pressed == 1 and page_in_loop: self._try_select_page(page) elif buttons_pressed > 1 or not page_in_loop: self._try_set_loop() if self._step_duplicator.is_duplicating: self._add_page_to_duplicator(page) if self.delete_button.is_pressed: self._clear_page(page) self._pressed_pages.append(page) absolute_page = page + self.page_offset if not self.select_button.is_pressed: if not liveobj_valid(self._sequencer_clip) and not self.song.view.highlighted_clip_slot.has_clip: create_clip(absolute_page) elif liveobj_valid(self._sequencer_clip): handle_page_press_on_clip(absolute_page) elif not self.is_following: self._try_select_page(absolute_page) def _try_select_page(self, page): step_time = page * self._page_length_in_beats if self._paginator.select_page_in_point(step_time): self.is_following = False return True return False def _try_set_loop(self): did_set_loop = False if liveobj_valid(self._sequencer_clip): if not clip_is_new_recording(self._sequencer_clip): lowest_page = min(self._pressed_pages) + self.page_offset if self._try_select_page(lowest_page): self._set_loop_in_live() did_set_loop = True if did_set_loop: self.is_following = True return did_set_loop def _set_loop_in_live(self): quant = self._page_length_in_beats start_page = min(self._pressed_pages) + self.page_offset end_page = max(self._pressed_pages) + self.page_offset loop_start = self._quantize_page_index(start_page, quant) loop_end = self._quantize_page_index(end_page, quant) + quant set_loop(self._sequencer_clip, loop_start, loop_end) self._sequencer_clip.view.show_loop() @property def _page_length_in_beats(self): return clamp(self._paginator.page_length, 0.25, self._one_measure_in_beats) @property def _one_measure_in_beats(self): return self._measure_length * self.song.signature_numerator / self.song.signature_denominator @property def page_offset(self): def zero_if_none(n): if n is None: return 0 else: return n width = zero_if_none(self.loop_selector_matrix.width) height = zero_if_none(self.loop_selector_matrix.height) size = max(width * height, 1) page_index = self._paginator.page_index page_length = self._paginator.page_length selected_page_index = int(page_index * page_length / self._page_length_in_beats) return size * int(selected_page_index / size)
class GeneralSettings(Subject): workflow = listenable_property.managed('scene')