class MonoNoteEditorComponent(NoteEditorComponent): """Custom function for displaying triplets for different grid sizes, called by _visible steps""" # _visible_steps_model = lambda self, indices: filter(lambda k: k % 4 != 3, indices) _visible_steps_model = lambda self, indices: [ k for k in indices if (k % 16) < 12 ] matrix = control_matrix(PadControl, channel=15, sensitivity_profile='loop', mode=PlayableControl.Mode.listenable) @matrix.pressed def matrix(self, button): super(MonoNoteEditorComponent, self)._on_pad_pressed(button.coordinate) @matrix.released def matrix(self, button): super(MonoNoteEditorComponent, self)._on_pad_released(button.coordinate) # def _on_pad_pressed(self, coordinate): # y, x = coordinate # #debug('MonoNoteEditorComponent._on_pad_pressed:', y, x) # super(MonoNoteEditorComponent, self)._on_pad_pressed(coordinate) def _visible_steps(self): first_time = self.page_length * self._page_index steps_per_page = self._get_step_count() step_length = self._get_step_length() indices = list(range(steps_per_page)) if is_triplet_quantization(self._triplet_factor): indices = [k for k in indices if k % 16 not in (12, 13, 14, 15)] return [(self._time_step(first_time + k * step_length), index) for k, index in enumerate(indices)]
class ColorChooserComponent(Component, Messenger): matrix = control_matrix(ButtonControl, dimensions=(8, 8)) def __init__(self, *a, **k): (super(ColorChooserComponent, self).__init__)(a, is_enabled=False, **k) self._object = None self._notification_ref = nop for button in self.matrix: row, column = button.coordinate button.color_index = COLOR_CHOOSER_LAYOUT[row][column] @property def object(self): return self._object @object.setter def object(self, obj): if liveobj_changed(self._object, obj): self._object = obj if obj is None: notification = self._notification_ref() if notification: notification.hide() self.set_enabled(False) else: self._render_color_palette( translate_color_index(obj.color_index)) self.set_enabled(True) self._notification_ref = self.show_notification( ('Select a color for: %s' % obj.name), notification_time=(-1)) @matrix.pressed def matrix(self, button): if liveobj_valid(self.object): if button.color_index is None: if old_hasattr(self.object, 'is_auto_colored'): self.object.is_auto_colored = True self.show_notification( 'Color automatically enabled for: %s' % self.object.name) else: self.object.color_index = inverse_translate_color_index( button.color_index) self.object = None def _render_color_palette(self, selected_color_index): for button in self.matrix: color_index = button.color_index if color_index is not None: if color_index == selected_color_index: button.color = Pulse( IndexedColor.from_push_index(color_index, shade_level=2), IndexedColor.from_push_index(color_index), SELECTION_PULSE_SPEED) else: button.color = IndexedColor.from_push_index(color_index) else: button.color = Rgb.BLACK
class MonoNoteEditorComponent(NoteEditorComponent): """Custom function for displaying triplets for different grid sizes, called by _visible steps""" _visible_steps_model = lambda self, indices: [ k for k in indices if k % 4 != 3 ] #_matrix = None matrix = control_matrix(PadControl, channel=15, sensitivity_profile='loop', mode=PlayableControl.Mode.listenable) """First we need to reset the state (chan, id) of each button of the matrix that was previously grabbed so that it doesn't display the playhead when it's given to something else""" """Next we need to override the channel that each control is set to in this function, as it is hardcoded from a header definition in the module""" """def set_button_matrix(self, matrix): if self._matrix: for button, _ in filter(first, self._matrix.iterbuttons()): button.reset_state() super(MonoNoteEditorComponent, self).set_button_matrix(matrix) if matrix: for button, _ in filter(first, matrix.iterbuttons()): button.set_channel(15)""" """ def set_matrix(self, matrix): if self._matrix: for button, _ in filter(first, self._matrix.iterbuttons()): button.reset_state() self._matrix = matrix super(MonoNoteEditorComponent, self).set_matrix(matrix) if matrix: for button, _ in filter(first, matrix.iterbuttons()): button.set_channel(15) """ @matrix.pressed def matrix(self, button): super(MonoNoteEditorComponent, self)._on_pad_pressed(button.coordinate) @matrix.released def matrix(self, button): super(MonoNoteEditorComponent, self)._on_pad_released(button.coordinate) def _on_pad_pressed(self, coordinate): y, x = coordinate debug('MonoNoteEditorComponent._on_pad_pressed:', y, x) super(MonoNoteEditorComponent, self)._on_pad_pressed(coordinate) def _visible_steps(self): first_time = self.page_length * self._page_index steps_per_page = self._get_step_count() step_length = self._get_step_length() indices = list(range(steps_per_page)) if is_triplet_quantization(self._triplet_factor): indices = self._visible_steps_model(indices) return [(self._time_step(first_time + k * step_length), index) for k, index in enumerate(indices)]
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 VelocityLevelsComponent(PlayableComponent): SOURCE_NOTES = list(reversed(range(64, 128))) DEFAULT_VELOCITY = 100 matrix = control_matrix(PadControl) select_button = ButtonControl() def __init__(self, velocity_levels=None, target_note_provider=None, skin_base_key=None, *a, **k): super(VelocityLevelsComponent, self).__init__(*a, **k) self._target_note_provider = target_note_provider or NullTargetNoteProvider( ) self.__on_selected_target_note_changed.subject = self._target_note_provider self._played_level = INVALID_LEVEL self.set_skin_base_key(skin_base_key or 'VelocityLevels') self._notification_task = self._tasks.add( task.run(self._update_velocity)) self._notification_task.kill() self.set_velocity_levels(velocity_levels) @listenable_property def velocity(self): if 0 <= self._played_level < 128: return self._played_level return self.DEFAULT_VELOCITY def set_velocities_playable(self, playable): self._notification_task.kill() self._set_control_pads_from_script(not playable) def set_velocity_levels(self, velocity_levels): self.velocity_levels = velocity_levels self.__on_last_played_level.subject = velocity_levels self.update() def set_matrix(self, matrix): super(VelocityLevelsComponent, self).set_matrix(matrix) self._update_sensitivity_profile() def _update_control_from_script(self): super(VelocityLevelsComponent, self)._update_control_from_script() self._update_sensitivity_profile() def _update_sensitivity_profile(self): profile = 'default' if self._takeover_pads else 'drums' for button in self.matrix: button.sensitivity_profile = profile def _update_velocity(self): self._notification_task.kill() self.notify_velocity() def set_skin_base_key(self, base_key): self._skin_base_key = base_key self._update_led_feedback() @matrix.pressed def matrix(self, button): self._on_matrix_pressed(button) @matrix.released def matrix(self, button): self._on_matrix_released(button) @select_button.pressed def select_button(self, _value): self._set_control_pads_from_script(True) @select_button.released def select_button(self, _value): self._set_control_pads_from_script(False) def _on_matrix_pressed(self, button): has_levels = liveobj_valid(self.velocity_levels) levels = self.velocity_levels.levels if has_levels else [] index = self._button_index(button) self._played_level = levels[index] if index < len( levels) else INVALID_LEVEL self._update_led_feedback() self.notify_velocity() @listens('selected_target_note') def __on_selected_target_note_changed(self): self.update() @listens('last_played_level') def __on_last_played_level(self): if self._takeover_pads or liveobj_valid(self.velocity_levels): played = self.velocity_levels.last_played_level if 1 else INVALID_LEVEL self._played_level = played self._update_led_feedback() self._notification_task.restart() def _button_index(self, button): y, x = button.coordinate return (self.height - 1 - y) * self.width + x def _note_translation_for_button(self, button): return (self.SOURCE_NOTES[self._button_index(button)], NON_FEEDBACK_CHANNEL) def _update_button_color(self, button): index = self._button_index(button) levels = self.velocity_levels.levels if liveobj_valid( self.velocity_levels) else [] if index < len(levels) and self._played_level == levels[index]: color = 'SelectedLevel' else: y, _ = button.coordinate color = 'MidLevel' if y == 0: color = 'HighLevel' else: if y == self.height - 1: color = 'LowLevel' button.color = self._skin_base_key + '.' + color def update(self): super(VelocityLevelsComponent, self).update() if liveobj_valid(self.velocity_levels): self.velocity_levels.enabled = self.is_enabled() self.velocity_levels.source_channel = NON_FEEDBACK_CHANNEL self.velocity_levels.notes = self.SOURCE_NOTES[:self.width * self.height] target_note = self._target_note_provider.selected_target_note self.velocity_levels.target_note = target_note.note self.velocity_levels.target_channel = target_note.channel if not self.is_enabled(): self._notification_task.kill()
class MonoDrumGroupComponent(DrumGroupComponent): mute_button = ButtonControl(color='DrumGroup.PadMuted') solo_button = ButtonControl(color='DrumGroup.PadSoloed') _raw_position = 0 _selected_note = DEFAULT_DRUMOFFSET * 4 select_matrix = control_matrix(PlayableControl) create_translation_entry = lambda self, button: (button.coordinate[ 1], button.coordinate[0], button.identifier, button.channel) def __init__(self, channel_list=CHANNELS, settings=DEFAULT_INSTRUMENT_SETTINGS, *a, **k): self._channel_list = channel_list self._settings = settings super(MonoDrumGroupComponent, self).__init__(*a, **k) self._on_selected_track_changed.subject = self.song.view def _get_current_channel(self): cur_track = self.song.view.selected_track cur_chan = cur_track.current_input_sub_routing if len(cur_chan) == 0: cur_chan = 'All Channels' if cur_chan == 'All Channels': cur_chan = 1 if cur_chan in self._channel_list: cur_chan = (self._channel_list.index(cur_chan) % 15) + 1 else: cur_chan = 14 return cur_chan @listens('selected_track') def _on_selected_track_changed(self): #debug('keys settings track changed') self.translation_channel = self._get_current_channel() self.update_matrix() @property def translation_channel(self, translation_channel): return self._translation_channel @translation_channel.setter def translation_channel(self, channel): debug('drumpad set_translation_channel', channel) self._translation_channel = channel self.update_matrix() @property def position(self): if liveobj_valid(self._drum_group_device): return self._drum_group_device.view.drum_pads_scroll_position return 0 @position.setter def position(self, index): if not 0 <= index <= 28: raise AssertionError self._raw_position = index if liveobj_valid(self._drum_group_device): self._drum_group_device.view.drum_pads_scroll_position = index self.update_matrix() else: self.update_matrix() def update_matrix(self): self._reset_selected_pads() self._update_led_feedback() self._update_note_translations() def set_translation_channel(self, translation_channel): self._translation_channel = translation_channel self._update_assigned_drum_pads() self._create_and_set_pad_translations() def _create_and_set_pad_translations(self): if self._can_set_pad_translations(): translations = [] for button in self.matrix: button.channel = self._translation_channel button.identifier = self._button_coordinates_to_pad_index( self._raw_position * 4, button.coordinate) button.enabled = True translations.append(self.create_translation_entry(button)) self._set_pad_translations(tuple(translations)) else: self._update_note_translations() self._set_pad_translations(None) def _button_coordinates_to_pad_index(self, first_note, coordinates): y, x = coordinates y = self.height - y - 1 if x < 4 and y >= 4: first_note += 32 elif x >= 4 and y < 4: first_note += 2 * self.width elif x >= 4 and y >= 4: first_note += 4 * self.width + 16 index = x % 4 + y % 4 * 4 + first_note return index def _note_translation_for_button(self, button): if liveobj_valid(self._drum_group_device): identifier = None channel = None if self.has_assigned_pads: identifier = self._button_coordinates_to_pad_index( first(self._assigned_drum_pads).note, button.coordinate) channel = self._translation_channel return (identifier, channel) else: identifier = self._button_coordinates_to_pad_index( self._raw_position * 4, button.coordinate) channel = self._translation_channel return (identifier, channel) def _update_led_feedback(self): if liveobj_valid(self._drum_group_device): super(MonoDrumGroupComponent, self)._update_led_feedback() else: super(DrumGroupComponent, self)._update_led_feedback() for button in self.select_matrix: self._update_button_color(button) def _update_button_color(self, button): if liveobj_valid(self._drum_group_device): super(MonoDrumGroupComponent, self)._update_button_color(button) elif button._control_element: note = self._button_coordinates_to_pad_index( self._raw_position * 4, button.coordinate) register = (note - 4) % 32 if note is self._selected_note: button.color = 'MonoInstrument.Drums.SelectedNote' else: button.color = 'MonoInstrument.Drums.EvenValue' if ( 0 <= register < 16) else 'MonoInstrument.Drums.OddValue' #debug('button color:', button.color) if button._control_element: button._control_element.scale_color = button.color def _color_for_pad(self, pad): has_soloed_pads = bool( find_if(lambda pad: pad.solo, self._all_drum_pads)) button_color = 'DrumGroup.PadEmpty' if pad == self._selected_drum_pad: button_color = 'DrumGroup.PadSelected' if has_soloed_pads and not pad.solo and not pad.mute: button_color = 'DrumGroup.PadSelectedNotSoloed' elif pad.mute and not pad.solo: button_color = 'DrumGroup.PadMutedSelected' elif has_soloed_pads and pad.solo: button_color = 'DrumGroup.PadSoloedSelected' elif pad.chains: register = (pad.note - 4) % 32 button_color = 'DrumGroup.PadFilled' if ( 0 <= register < 16) else 'DrumGroup.PadFilledAlt' if has_soloed_pads and not pad.solo: button_color = 'DrumGroup.PadFilled' if not pad.mute else 'DrumGroup.PadMuted' elif not has_soloed_pads and pad.mute: button_color = 'DrumGroup.PadMuted' elif has_soloed_pads and pad.solo: button_color = 'DrumGroup.PadSoloed' return button_color def set_drum_group_device(self, *a, **k): super(MonoDrumGroupComponent, self).set_drum_group_device(*a, **k) self.update_matrix() def _update_selected_drum_pad(self): super(MonoDrumGroupComponent, self)._update_selected_drum_pad() self.notify_selected_note() @listenable_property def selected_note(self): selected_drum_pad = self._drum_group_device.view.selected_drum_pad if liveobj_valid( self._drum_group_device) else None if liveobj_valid(selected_drum_pad): debug('selected note:', selected_drum_pad.note) return selected_drum_pad.note else: debug('selected note:', self._selected_note) return int(self._selected_note) def set_matrix(self, matrix): self._set_matrix_special_attributes(False) super(MonoDrumGroupComponent, self).set_matrix(matrix) self._set_matrix_special_attributes(True) def _set_matrix_special_attributes(self, enabled): for button in self.matrix: if button._control_element: button._control_element.display_press = enabled button._control_element._last_flash = 0 not enabled and button._control_element.reset_state() def _on_matrix_pressed(self, button): super(MonoDrumGroupComponent, self)._on_matrix_pressed(button) @select_matrix.pressed def select_matrix(self, button): debug('on select matrix pressed:', button) if liveobj_valid(self._drum_group_device): if self.mute_button.is_pressed or self.solo_button.is_pressed: super(MonoDrumGroupComponent, self)._on_matrix_pressed(button) else: self._drum_group_device.view.selected_drum_pad = self._pad_for_button( button) else: self._selected_note = self._button_coordinates_to_pad_index( self._raw_position * 4, button.coordinate) self.notify_selected_note() self._update_led_feedback() def select_drum_pad(self, drum_pad): pass def set_select_matrix(self, matrix): debug('set select matrix:', matrix) self.select_matrix.set_control_element(matrix) for button in self.select_matrix: button.set_playable(False) self._update_led_feedback() def _update_note_translations(self): if liveobj_valid(self._drum_group_device): super(MonoDrumGroupComponent, self)._update_note_translations() else: super(DrumGroupComponent, self)._update_note_translations() def _button_should_be_enabled(self, button): #this was throwing an error so I overrode, sometimes drumcomponent sends a non-iterable identifier = 128 if button: identifier, _ = self._note_translation_for_button(button) return identifier < 128 def _on_selected_drum_pad_changed(self): debug('on selected drumpad changed') self.notify_selected_note()
class InstrumentComponent(PlayableComponent, Slideable, Messenger): __events__ = ('pattern', ) matrix = control_matrix(PadControl) delete_button = ButtonControl() def __init__(self, note_layout=None, *a, **k): (super(InstrumentComponent, self).__init__)(*a, **k) self._note_layout = note_layout self._first_note = self.page_length * 3 + self.page_offset self._last_page_length = self.page_length self._last_page_offset = self.page_offset self._detail_clip = None self._has_notes = [False] * 128 self._has_notes_pattern = self._get_pattern(0) self._aftertouch_control = None self._aftertouch_mode = 'mono' self._show_notifications = True self._InstrumentComponent__on_detail_clip_changed.subject = self.song.view self._InstrumentComponent__on_detail_clip_changed() self._slider = SlideComponent(slideable=self, parent=self) self._touch_slider = SlideableTouchStripComponent(touch_slideable=self, parent=self) for event in ('scale', 'root_note', 'is_in_key', 'is_fixed', 'is_horizontal', 'interval'): self.register_slot(self._note_layout, self._on_note_layout_changed, event) self._update_pattern() @listens('detail_clip') def __on_detail_clip_changed(self): clip = self.song.view.detail_clip self.set_detail_clip(clip if (liveobj_valid(clip)) and ( clip.is_midi_clip) else None) def set_detail_clip(self, clip): if clip != self._detail_clip: self._detail_clip = clip self._on_clip_notes_changed.subject = clip self._on_loop_start_changed.subject = clip self._on_loop_end_changed.subject = clip self._on_clip_notes_changed() @listens('notes') def _on_clip_notes_changed(self): if self._detail_clip: self._has_notes = [False] * 128 loop_start = self._detail_clip.loop_start loop_length = self._detail_clip.loop_end - loop_start notes = self._detail_clip.get_notes_extended(from_time=loop_start, from_pitch=0, time_span=loop_length, pitch_span=128) for note in notes: self._has_notes[note.pitch] = True self.notify_contents() @listens('loop_start') def _on_loop_start_changed(self): self._on_loop_selection_changed() @listens('loop_end') def _on_loop_end_changed(self): self._on_loop_selection_changed() def _on_loop_selection_changed(self): self._on_clip_notes_changed() def contents(self, index): if self._detail_clip: note = self._has_notes_pattern[index].index if note is not None: return self._has_notes[note] return False return False @property def show_notifications(self): return self._show_notifications @show_notifications.setter def show_notifications(self, value): self._show_notifications = value @property def page_length(self): if self._note_layout.is_in_key: return len(self._note_layout.notes) return 12 @property def position_count(self): if not self._note_layout.is_in_key: return 139 offset = self.page_offset octaves = 11 if self._note_layout.notes[0] < 8 else 10 return offset + len(self._note_layout.notes) * octaves def _first_scale_note_offset(self): if not self._note_layout.is_in_key: return self._note_layout.notes[0] if self._note_layout.notes[0] == 0: return 0 return len(self._note_layout.notes) - index_if(lambda n: n >= 12, self._note_layout.notes) @property def page_offset(self): if self._note_layout.is_fixed: return 0 return self._first_scale_note_offset() def _get_position(self): return self._first_note def _set_position(self, note): self._first_note = note self._update_pattern() self._update_matrix() self.notify_position() position = property(_get_position, _set_position) @property def min_pitch(self): return self.pattern[0].index @property def max_pitch(self): identifiers = [control.identifier for control in self.matrix] if len(identifiers) > 0: return max(identifiers) return 127 @property def pattern(self): return self._pattern @matrix.pressed def matrix(self, button): self._on_matrix_pressed(button) def _on_matrix_pressed(self, button): if self.delete_button.is_pressed: pitch = self._get_note_info_for_coordinate(button.coordinate).index if pitch: if self._detail_clip: self._do_delete_pitch(pitch) @matrix.released def matrix(self, button): self._on_matrix_released(button) def _on_matrix_released(self, button): pass def _do_delete_pitch(self, pitch): clip = self._detail_clip if clip: note_name = pitch_index_to_string(pitch) loop_length = clip.loop_end - clip.loop_start clip.remove_notes_extended(from_time=(clip.loop_start), from_pitch=pitch, time_span=loop_length, pitch_span=1) self.show_notification(consts.MessageBoxText.DELETE_NOTES % note_name) @delete_button.pressed def delete_button(self, value): self._set_control_pads_from_script(True) @delete_button.released def delete_button(self, value): self._set_control_pads_from_script(False) def set_note_strip(self, strip): self._touch_slider.set_scroll_strip(strip) def set_octave_strip(self, strip): self._touch_slider.set_page_strip(strip) def set_octave_up_button(self, button): self._slider.set_scroll_page_up_button(button) def set_octave_down_button(self, button): self._slider.set_scroll_page_down_button(button) def set_scale_up_button(self, button): self._slider.set_scroll_up_button(button) def set_scale_down_button(self, button): self._slider.set_scroll_down_button(button) def set_aftertouch_control(self, control): self._aftertouch_control = control self._update_aftertouch() def set_aftertouch_mode(self, mode): if self._aftertouch_mode != mode: self._aftertouch_mode = mode self._update_aftertouch() def _align_first_note(self): self._first_note = self.page_offset + ( self._first_note - self._last_page_offset) * old_div( float(self.page_length), float(self._last_page_length)) if self._first_note >= self.position_count: self._first_note -= self.page_length self._last_page_length = self.page_length self._last_page_offset = self.page_offset def _on_note_layout_changed(self, _): self._update_scale() def show_pitch_range_notification(self): if self.is_enabled(): if self.show_notifications: self.show_notification( 'Play {start_note} to {end_note}'.format( start_note=(pitch_index_to_string( self.pattern.note(0, 0).index)), end_note=(pitch_index_to_string( self.pattern.note(self.width - 1, self.height - 1).index)))) def _update_scale(self): self._align_first_note() self._update_pattern() self._update_matrix() self.notify_position_count() self.notify_position() self.notify_contents() def update(self): super(InstrumentComponent, self).update() if self.is_enabled(): self._update_matrix() self._update_aftertouch() def _update_pattern(self): self._pattern = self._get_pattern() self._has_notes_pattern = self._get_pattern(0) self.notify_pattern() def _invert_and_swap_coordinates(self, coordinates): return (coordinates[1], self.height - 1 - coordinates[0]) def _get_note_info_for_coordinate(self, coordinate): x, y = self._invert_and_swap_coordinates(coordinate) return self.pattern.note(x, y) def _update_button_color(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) button.color = 'Instrument.' + note_info.color def _button_should_be_enabled(self, button): return self._get_note_info_for_coordinate( button.coordinate).index != None def _note_translation_for_button(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) return (note_info.index, note_info.channel) def _set_button_control_properties(self, button): super(InstrumentComponent, self)._set_button_control_properties(button) button.sensitivity_profile = 'default' if self._takeover_pads else 'instrument' def _update_matrix(self): self._update_control_from_script() self._update_note_translations() self._update_led_feedback() def _get_pattern(self, first_note=None): if first_note is None: first_note = int(round(self._first_note)) interval = self._note_layout.interval notes = self._note_layout.notes width = None height = None octave = old_div(first_note, self.page_length) offset = first_note % self.page_length - self._first_scale_note_offset( ) if interval == None: if self._note_layout.is_in_key: interval = len(self._note_layout.notes) if self._note_layout.is_horizontal: width = interval + 1 else: height = interval + 1 else: interval = 8 elif not self._note_layout.is_in_key: interval = [0, 2, 4, 5, 7, 9, 10, 11][interval] if self._note_layout.is_horizontal: steps = [1, interval] origin = [offset, 0] else: steps = [interval, 1] origin = [0, offset] return MelodicPattern(steps=steps, scale=notes, origin=origin, root_note=(octave * 12), chromatic_mode=(not self._note_layout.is_in_key), width=width, height=height) def _update_aftertouch(self): if self.is_enabled(): if self._aftertouch_control != None: self._aftertouch_control.send_value(self._aftertouch_mode)
class DrumGroupComponent(SlideableTouchStripComponent, DrumGroupComponent, Messenger): """ Class representing a drum group pads in a matrix. """ matrix = control_matrix(PadControl) duplicate_button = ButtonControl() def __init__(self, quantizer=None, *a, **k): super(DrumGroupComponent, self).__init__(touch_slideable=self, translation_channel=PAD_FEEDBACK_CHANNEL, dragging_enabled=True, *a, **k) self._copy_handler = self._make_copy_handler() self._notification_reference = partial(nop, None) self._quantizer = quantizer return position_count = 32 page_length = 4 page_offset = 1 def update(self): super(DrumGroupComponent, self).update() if self._copy_handler: self._copy_handler.stop_copying() def set_drum_group_device(self, drum_group_device): super(DrumGroupComponent, self).set_drum_group_device(drum_group_device) self._on_chains_changed.subject = self._drum_group_device self.notify_contents() def quantize_pitch(self, note): self._quantizer.quantize_pitch(note, 'pad') def _update_selected_drum_pad(self): super(DrumGroupComponent, self)._update_selected_drum_pad() self.notify_selected_note() self.notify_selected_target_note() def _update_assigned_drum_pads(self): super(DrumGroupComponent, self)._update_assigned_drum_pads() self.notify_selected_target_note() def _make_copy_handler(self): return DrumPadCopyHandler(self.show_notification) @matrix.pressed def matrix(self, pad): self._on_matrix_pressed(pad) @matrix.released def matrix(self, pad): self._on_matrix_released(pad) def _on_matrix_pressed(self, pad): super(DrumGroupComponent, self)._on_matrix_pressed(pad) if self.duplicate_button.is_pressed: self._duplicate_pad(pad) def set_select_button(self, button): self.select_button.set_control_element(button) def set_mute_button(self, button): self.mute_button.set_control_element(button) def set_solo_button(self, button): self.solo_button.set_control_element(button) def set_quantize_button(self, button): self.quantize_button.set_control_element(button) def set_delete_button(self, button): self.delete_button.set_control_element(button) @duplicate_button.pressed def duplicate_button(self, button): self._set_control_pads_from_script(True) @duplicate_button.released def duplicate_button(self, button): self._set_control_pads_from_script(False) if self._copy_handler: self._copy_handler.stop_copying() if self._notification_reference() is not None: self._notification_reference().hide() return @listens('chains') def _on_chains_changed(self): self._update_led_feedback() self.notify_contents() def delete_pitch(self, drum_pad): clip = self.song.view.detail_clip if clip and any(clip.get_notes(0, drum_pad.note, DISTANT_FUTURE, 1)): clip.remove_notes(0, drum_pad.note, DISTANT_FUTURE, 1) self.show_notification(MessageBoxText.DELETE_NOTES % drum_pad.name) else: self.show_notification(MessageBoxText.DELETE_DRUM_RACK_PAD % drum_pad.name) self.delete_drum_pad_content(drum_pad) def delete_drum_pad_content(self, drum_pad): drum_pad.delete_all_chains() def _duplicate_pad(self, pad): if self._copy_handler and self._drum_group_device: drum_pad = self._pad_for_button(pad) self._notification_reference = self._copy_handler.duplicate_pad( self._drum_group_device, drum_pad) def set_matrix(self, matrix): super(DrumGroupComponent, self).set_matrix(matrix) self._update_sensitivity_profile() def _update_control_from_script(self): super(DrumGroupComponent, self)._update_control_from_script() self._update_sensitivity_profile() def _update_sensitivity_profile(self): profile = 'default' if self._takeover_pads or self._selected_pads else 'drums' for button in self.matrix: button.sensitivity_profile = profile @listenable_property def selected_note(self): if liveobj_valid(self._selected_drum_pad): return self._selected_drum_pad.note return -1 @listenable_property def selected_target_note(self): note_and_channel = (-1, -1) if liveobj_valid(self._drum_group_device) and liveobj_valid( self._selected_drum_pad): if self._selected_drum_pad in self.assigned_drum_pads: predicate = lambda b: self._pad_for_button( b) == self._selected_drum_pad button = find_if(predicate, self.matrix) if button != None and None not in (button.identifier, button.channel): note_and_channel = (button.identifier, button.channel) else: note_and_channel = (self._selected_drum_pad.note, NON_FEEDBACK_CHANNEL) return NamedTuple(note=note_and_channel[0], channel=note_and_channel[1])
class InstrumentComponent(PlayableComponent, CompoundComponent, Slideable, Messenger): """ Class that sets up the button matrix as a piano, using different selectable layouts for the notes. """ __events__ = ('pattern', ) touch_strip_toggle = ToggleButtonControl() matrix = control_matrix(PadControl, pressed_color='Instrument.NoteAction') def __init__(self, note_layout=None, *a, **k): raise note_layout is not None or AssertionError super(InstrumentComponent, self).__init__(*a, **k) self._note_layout = note_layout self._delete_button = None self._first_note = self.page_length * 3 + self.page_offset self._last_page_length = self.page_length self._delete_button = None self._last_page_offset = self.page_offset self._touch_strip = None self._touch_strip_indication = None self._detail_clip = None self._has_notes = [False] * 128 self._has_notes_pattern = self._get_pattern(0) self._aftertouch_control = None self._slider = self.register_component(SlideComponent(self)) self._touch_slider = self.register_component( SlideableTouchStripComponent(self)) for event in ('scale', 'root_note', 'is_in_key', 'is_fixed', 'is_horizontal', 'interval'): self.register_slot(self._note_layout, self._on_note_layout_changed, event) self._update_pattern() def set_detail_clip(self, clip): if clip != self._detail_clip: self._detail_clip = clip self._on_clip_notes_changed.subject = clip self._on_loop_start_changed.subject = clip self._on_loop_end_changed.subject = clip self._on_clip_notes_changed() @listens('notes') def _on_clip_notes_changed(self): if self._detail_clip: self._has_notes = [False] * 128 loop_start = self._detail_clip.loop_start loop_length = self._detail_clip.loop_end - loop_start notes = self._detail_clip.get_notes(loop_start, 0, loop_length, 128) for note in notes: self._has_notes[note[0]] = True self.notify_contents() @listens('loop_start') def _on_loop_start_changed(self): self._on_loop_selection_changed() @listens('loop_end') def _on_loop_end_changed(self): self._on_loop_selection_changed() def _on_loop_selection_changed(self): self._on_clip_notes_changed() def contents(self, index): if self._detail_clip: note = self._has_notes_pattern[index].index return self._has_notes[note] if note is not None else False return False @property def page_length(self): return len( self._note_layout.notes) if self._note_layout.is_in_key else 12 @property def position_count(self): if not self._note_layout.is_in_key: return 139 else: offset = self.page_offset octaves = 11 if self._note_layout.notes[0] < 8 else 10 return offset + len(self._note_layout.notes) * octaves def _first_scale_note_offset(self): if not self._note_layout.is_in_key: return self._note_layout.notes[0] elif self._note_layout.notes[0] == 0: return 0 else: return len(self._note_layout.notes) - index_if( lambda n: n >= 12, self._note_layout.notes) @property def page_offset(self): return 0 if self._note_layout.is_fixed else self._first_scale_note_offset( ) def _get_position(self): return self._first_note def _set_position(self, note): self._first_note = note self._update_pattern() self._update_matrix() self.notify_position() position = property(_get_position, _set_position) @property def pattern(self): return self._pattern @matrix.pressed def matrix(self, button): self._on_matrix_pressed(button) def _on_matrix_pressed(self, button): if self._delete_button and self._delete_button.is_pressed(): pitch = self._get_note_info_for_coordinate(button.coordinate).index if pitch and self._detail_clip: self._do_delete_pitch(pitch) def _do_delete_pitch(self, pitch): clip = self._detail_clip if clip: note_name = pitch_index_to_string(pitch) loop_length = clip.loop_end - clip.loop_start clip.remove_notes(clip.loop_start, pitch, loop_length, 1) self.show_notification(consts.MessageBoxText.DELETE_NOTES % note_name) @listens('value') def _on_delete_value(self, value): self._set_control_pads_from_script(bool(value)) @touch_strip_toggle.toggled def touch_strip_toggle(self, toggled, button): self._update_touch_strip() self._update_touch_strip_indication() self.show_notification( consts.MessageBoxText.TOUCHSTRIP_MODWHEEL_MODE if toggled else consts.MessageBoxText.TOUCHSTRIP_PITCHBEND_MODE) def set_touch_strip(self, control): self._touch_strip = control self._update_touch_strip() def _update_touch_strip(self): if self._touch_strip: self._touch_strip.behaviour = MODWHEEL_BEHAVIOUR if self.touch_strip_toggle.is_toggled else DEFAULT_BEHAVIOUR def set_touch_strip_indication(self, control): self._touch_strip_indication = control self._update_touch_strip_indication() def _update_touch_strip_indication(self): if self._touch_strip_indication: self._touch_strip_indication.set_mode(TouchStripModes.CUSTOM_FREE) self._touch_strip_indication.send_state([ (TouchStripStates.STATE_FULL if self.touch_strip_toggle.is_toggled else TouchStripStates.STATE_HALF) for _ in xrange(self._touch_strip_indication.state_count) ]) def set_note_strip(self, strip): self._touch_slider.set_scroll_strip(strip) def set_octave_strip(self, strip): self._touch_slider.set_page_strip(strip) def set_octave_up_button(self, button): self._slider.set_scroll_page_up_button(button) def set_octave_down_button(self, button): self._slider.set_scroll_page_down_button(button) def set_scale_up_button(self, button): self._slider.set_scroll_up_button(button) def set_scale_down_button(self, button): self._slider.set_scroll_down_button(button) def set_aftertouch_control(self, control): self._aftertouch_control = control self._update_aftertouch() def set_delete_button(self, button): self._delete_button = button self._on_delete_value.subject = button self._set_control_pads_from_script(button and button.is_pressed()) def _align_first_note(self): self._first_note = self.page_offset + ( self._first_note - self._last_page_offset) * float( self.page_length) / float(self._last_page_length) if self._first_note >= self.position_count: self._first_note -= self.page_length self._last_page_length = self.page_length self._last_page_offset = self.page_offset def _on_note_layout_changed(self, _): self._update_scale() def _update_scale(self): self._align_first_note() self._update_pattern() self._update_matrix() self.notify_position_count() self.notify_position() self.notify_contents() def update(self): super(InstrumentComponent, self).update() if self.is_enabled(): self._update_matrix() self._update_aftertouch() self._update_touch_strip() self._update_touch_strip_indication() def _update_pattern(self): self._pattern = self._get_pattern() self._has_notes_pattern = self._get_pattern(0) self.notify_pattern() def _invert_and_swap_coordinates(self, coordinates): return (coordinates[1], self.width - 1 - coordinates[0]) def _get_note_info_for_coordinate(self, coordinate): x, y = self._invert_and_swap_coordinates(coordinate) return self.pattern.note(x, y) def _update_button_color(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) button.color = 'Instrument.' + note_info.color def _button_should_be_enabled(self, button): return self._get_note_info_for_coordinate( button.coordinate).index != None def _note_translation_for_button(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) return (note_info.index, note_info.channel) def _set_button_control_properties(self, button): super(InstrumentComponent, self)._set_button_control_properties(button) button.sensitivity_profile = 'default' if self._takeover_pads else 'instrument' def _update_matrix(self): self._update_control_from_script() self._update_led_feedback() self._update_note_translations() def _get_pattern(self, first_note=None): if first_note is None: first_note = int(round(self._first_note)) interval = self._note_layout.interval notes = self._note_layout.notes octave = first_note / self.page_length offset = first_note % self.page_length - self._first_scale_note_offset( ) if interval == None: interval = 8 elif not self._note_layout.is_in_key: interval = [0, 2, 4, 5, 7, 9, 10, 11][interval] if self._note_layout.is_horizontal: steps = [1, interval] origin = [offset, 0] else: steps = [interval, 1] origin = [0, offset] return MelodicPattern(steps=steps, scale=notes, origin=origin, root_note=octave * 12, chromatic_mode=not self._note_layout.is_in_key) def _update_aftertouch(self): if self.is_enabled() and self._aftertouch_control != None: self._aftertouch_control.send_value('mono')
class NoteEditorComponent(Component): __events__ = (u'page_length', u'active_note_regions', u'active_steps', u'notes_changed', u'modify_all_notes') matrix = control_matrix(PadControl, channel=PLAYHEAD_FEEDBACK_CHANNELS[0], sensitivity_profile='loop', mode=PlayableControl.Mode.listenable) mute_button = ButtonControl(color='DefaultButton.Transparent') def __init__(self, clip_creator=None, grid_resolution=None, skin_base_key='NoteEditor', velocity_range_thresholds=None, velocity_provider=None, get_notes_handler=get_single_note, remove_notes_handler=remove_single_note, duplicate_all_notes=False, *a, **k): assert skin_base_key is not None super(NoteEditorComponent, self).__init__(*a, **k) self._duplicate_all_notes = duplicate_all_notes self._get_notes_handler = get_notes_handler self._remove_notes_handler = remove_notes_handler self._skin_base_key = skin_base_key self.full_velocity = False self._provided_velocity = None self._selected_page_point = 0 self._page_index = 0 self._clip_creator = clip_creator self._sequencer_clip = None self._step_colors = [] self._pressed_steps = [] self._modified_steps = [] self._pressed_step_callback = None self._modify_task = self._tasks.add(task.run(self._do_modification)) self._modify_task.kill() self._modify_all_notes_enabled = False self._step_tap_tasks = {} self._clip_notes = [] self._pitches = [ DEFAULT_START_NOTE] self._grid_resolution = grid_resolution self.__on_resolution_changed.subject = self._grid_resolution self.set_step_duplicator(None) self._nudge_offset = 0 self._length_offset = 0 self._velocity_offset = 0 self._triplet_factor = 1.0 self._update_from_grid() self.background_color = self._skin_base_key + '.StepEmpty' self._velocity_range_thresholds = velocity_range_thresholds or DEFAULT_VELOCITY_RANGE_THRESHOLDS self._velocity_provider = velocity_provider or NullVelocityProvider() self.__on_provided_velocity_changed.subject = self._velocity_provider return @property def page_index(self): return self._page_index @property def page_length(self): return self._get_step_count() * self._get_step_length() * self._triplet_factor @property def can_change_page(self): return not self._pressed_steps and not self._modified_steps @property def notes_in_selected_step(self): time_steps = [] if self._modify_all_notes_enabled: time_steps = [ TimeStep(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)] else: time_steps = [ self._time_step(self.get_step_start_time(start_time)) for start_time in chain(self._pressed_steps, self._modified_steps) ] time_steps_with_notes = [ time_step.filter_notes(self._clip_notes) for time_step in time_steps ] notes_in_step = [] for time_step in time_steps_with_notes: for note in time_step: notes_in_step.append(note_pitch(note)) return notes_in_step def set_selected_page_point(self, point): assert self.can_change_page self._selected_page_point = point index = int(point / self.page_length) if self.page_length != 0 else 0 if index != self._page_index: self._page_index = index self._on_clip_notes_changed() def _get_modify_all_notes_enabled(self): return self._modify_all_notes_enabled def _set_modify_all_notes_enabled(self, enabled): if enabled != self._modify_all_notes_enabled: self._modify_all_notes_enabled = enabled self._on_clip_notes_changed() self.notify_modify_all_notes() modify_all_notes_enabled = property(_get_modify_all_notes_enabled, _set_modify_all_notes_enabled) def set_detail_clip(self, clip): if liveobj_changed(clip, self._sequencer_clip): self._sequencer_clip = clip self._step_duplicator.set_clip(clip) if self.can_change_page: self.set_selected_page_point(0) self._on_clip_notes_changed.subject = clip self._on_clip_notes_changed() def _can_edit(self): return len(self._pitches) != 0 def _get_editing_notes(self): return self._pitches def _set_editing_notes(self, pitches): self._pitches = pitches enabled = self._can_edit() for button in self.matrix: button.enabled = enabled if enabled: self._on_clip_notes_changed() editing_notes = property(_get_editing_notes, _set_editing_notes) def _get_width(self): if self.matrix.width: return self.matrix.width return 4 def _get_height(self): if self.matrix.height: return self.matrix.height return 4 def set_matrix(self, matrix): last_page_length = self.page_length self.matrix.set_control_element(matrix) for t in self._step_tap_tasks.itervalues(): t.kill() def trigger_modification_task(x, y): trigger = partial(self._trigger_modification, (x, y), done=True) return self._tasks.add(task.sequence(task.wait(defaults.MOMENTARY_DELAY), task.run(trigger))).kill() self._step_tap_tasks = dict([ ((x, y), trigger_modification_task(x, y)) for x, y in product(xrange(self._get_width()), xrange(self._get_height())) ]) if matrix and last_page_length != self.page_length: self._on_clip_notes_changed() self.notify_page_length() else: self._update_editor_matrix() def set_step_duplicator(self, duplicator): self._step_duplicator = duplicator or NullStepDuplicator() self._step_duplicator.set_clip(self._sequencer_clip) def update(self): super(NoteEditorComponent, self).update() self._update_editor_matrix_leds() def _get_clip_notes_time_range(self): if self._modify_all_notes_enabled: time_length = self._get_step_count() * 4.0 time_start = 0 else: time_length = self.page_length time_start = self._page_index * time_length return (time_start - self._time_step(0).offset, time_length) @listens('notes') def _on_clip_notes_changed(self): u""" get notes from clip for offline array """ if liveobj_valid(self._sequencer_clip) and self._can_edit(): time_start, time_length = self._get_clip_notes_time_range() self._clip_notes = self._get_notes_handler(self._sequencer_clip, time_start, self._pitches, time_length) else: self._clip_notes = [] self._update_editor_matrix() self.notify_notes_changed() def _update_editor_matrix(self): u""" update offline array of button LED values, based on note velocity and mute states """ step_colors = [ self._skin_base_key + '.StepDisabled'] * self._get_step_count() def coords_to_index(coord): return coord[0] + coord[1] * self._get_width() editing_indices = set(map(coords_to_index, self._modified_steps)) selected_indices = set(map(coords_to_index, self._pressed_steps)) last_editing_notes = [] for time_step, index in self._visible_steps(): notes = time_step.filter_notes(self._clip_notes) if len(notes) > 0: last_editing_notes = [] if index in selected_indices: color = self._skin_base_key + '.StepSelected' elif index in editing_indices: note_color = self._determine_color(notes) color = self._skin_base_key + '.StepEditing.' + note_color last_editing_notes = notes else: note_color = self._determine_color(notes) color = self._skin_base_key + '.Step.' + note_color elif any(imap(time_step.overlaps_note, last_editing_notes)): color = self._skin_base_key + '.StepEditing.' + note_color elif index in editing_indices or index in selected_indices: color = self._skin_base_key + '.StepSelected' last_editing_notes = [] else: color = self.background_color last_editing_notes = [] step_colors[index] = color self._step_colors = step_colors self._update_editor_matrix_leds() def _determine_color(self, notes): return color_for_note(most_significant_note(notes), velocity_range_thresholds=self._velocity_range_thresholds) def _visible_steps(self): first_time = self.page_length * self._page_index steps_per_page = self._get_step_count() step_length = self._get_step_length() indices = range(steps_per_page) if is_triplet_quantization(self._triplet_factor): indices = filter(lambda k: k % 8 not in (6, 7), indices) return [ (self._time_step(first_time + k * step_length), index) for k, index in enumerate(indices) ] def _update_editor_matrix_leds(self): if self.is_enabled(): for control in self.matrix: control.color = self._step_colors[control.index] def _get_step_count(self): return self._get_width() * self._get_height() def get_step_start_time(self, step): x, y = step assert in_range(x, 0, self._get_width()) assert in_range(y, 0, self._get_height()) page_time = self._page_index * self._get_step_count() * self._triplet_factor step_time = x + y * self._get_width() * self._triplet_factor return (page_time + step_time) * self._get_step_length() def _get_step_length(self): return self._grid_resolution.step_length def get_row_start_times(self): return [ self.get_step_start_time((0, row)) for row in xrange(self._get_height()) ] def _update_from_grid(self): quantization, is_triplet = self._grid_resolution.clip_grid self._triplet_factor = 1.0 if not is_triplet else 0.75 if self._clip_creator: self._clip_creator.grid_quantization = quantization self._clip_creator.is_grid_triplet = is_triplet if liveobj_valid(self._sequencer_clip): self._sequencer_clip.view.grid_quantization = quantization self._sequencer_clip.view.grid_is_triplet = is_triplet @mute_button.pressed def mute_button(self, button): self._trigger_modification(immediate=True) @listens('index') def __on_resolution_changed(self, *a): self._release_active_steps() self._update_from_grid() self.set_selected_page_point(self._selected_page_point) self.notify_page_length() self._on_clip_notes_changed() @matrix.pressed def matrix(self, button): self._on_pad_pressed(button.coordinate) @matrix.released def matrix(self, button): self._on_pad_released(button.coordinate) def _on_pad_pressed(self, coordinate): y, x = coordinate if self.is_enabled(): if not liveobj_valid(self._sequencer_clip): self.set_detail_clip(create_clip_in_selected_slot(self._clip_creator, self.song)) if self._can_press_or_release_step(x, y): self._on_press_step((x, y)) self._update_editor_matrix() def _on_pad_released(self, coordinate): y, x = coordinate if self.is_enabled() and self._can_press_or_release_step(x, y): self._on_release_step((x, y)) self._update_editor_matrix() def _can_press_or_release_step(self, x, y): width = self._get_width() * self._triplet_factor if is_triplet_quantization(self._triplet_factor) else self._get_width() return x < width and y < self._get_height() @listens('velocity') def __on_provided_velocity_changed(self): if len(self._pressed_steps) + len(self._modified_steps) > 0: self._provided_velocity = self._velocity_provider.velocity self._trigger_modification(immediate=True) self._provided_velocity = None return def _get_step_time_range(self, step): time = self.get_step_start_time(step) return ( time, time + self._get_step_length()) @property def editing_note_regions(self): active_time_spans = self.active_time_spans editing_notes = set(self.notes_in_selected_step) if len(editing_notes) > 1: editing_notes = [ -1] return list(product(editing_notes, list(active_time_spans))) @property def active_time_spans(self): if self._modify_all_notes_enabled: return [(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)] else: def time_span(step): time_step = self._time_step(self.get_step_start_time(step)) return ( time_step.left_boundary(), time_step.right_boundary()) return imap(time_span, chain(self._pressed_steps, self._modified_steps)) @property def active_steps(self): return imap(self._get_step_time_range, chain(self._pressed_steps, self._modified_steps)) @property def active_note_regions(self): return imap(self._get_time_range, chain(self._pressed_steps, self._modified_steps)) def _get_time_range(self, step): def note_compare(left, right): return note_start_time(left) - note_start_time(right) time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) if notes: beginning_note = first(sorted(notes, key=cmp_to_key(note_compare))) start = note_start_time(beginning_note) end = start + note_length(beginning_note) if len(notes) > 1: end_note = notes[(-1)] end = note_start_time(end_note) + note_length(end_note) return (start, end) else: return ( time, time + self._get_step_length()) def _release_active_steps(self): for step in self._pressed_steps + self._modified_steps: self._on_release_step(step, do_delete_notes=False) def _on_release_step(self, step, do_delete_notes=True): self._step_tap_tasks[step].kill() if step in self._pressed_steps: if do_delete_notes: self._delete_notes_in_step(step) self._pressed_steps.remove(step) for pitch in self._pitches: self._add_or_modify_note_in_step(step, pitch) if step in self._modified_steps: self._modified_steps.remove(step) if len(self._modified_steps) + len(self._pressed_steps) == 0: self._velocity_provider.set_velocities_playable(True) self.notify_active_steps() self.notify_active_note_regions() def _find_continued_step(self, step): def steps_to_note_start(steps): return map(lambda step: self.get_step_start_time(step), steps) time = self.get_step_start_time(step) all_steps_with_notes = [ time_step for time_step, index in self._visible_steps() if time_step.filter_notes(self._clip_notes) ] all_time_step_starts = map(lambda ts: ts.start, all_steps_with_notes) if all_time_step_starts: insert_point = bisect(all_time_step_starts, time) if insert_point > 0: prev_filled_step = all_steps_with_notes[(insert_point - 1)] notes_in_modified_steps = steps_to_note_start(self._modified_steps) if prev_filled_step.start in notes_in_modified_steps: return prev_filled_step return def _add_step_to_duplicator(self, step): nudge_offset = 0 time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) step_start, step_end = self._get_time_range(step) if notes: nudge_offset = min(imap(note_start_time, notes)) - time if self._duplicate_all_notes: self._step_duplicator.add_step(step_start, step_end, nudge_offset) else: self._step_duplicator.add_step_with_pitch(self.editing_notes[0], step_start, step_end, nudge_offset) def _on_press_step(self, step): if liveobj_valid(self._sequencer_clip) and step not in self._pressed_steps and step not in self._modified_steps: if self._step_duplicator.is_duplicating: self._add_step_to_duplicator(step) else: self._step_tap_tasks[step].restart() continued_step = self._find_continued_step(step) if continued_step: self._modify_length_of_notes_within_existing_step(continued_step, step) else: self._pressed_steps.append(step) self._velocity_provider.set_velocities_playable(False) self.notify_active_steps() self.notify_active_note_regions() def _modify_length_of_notes_within_existing_step(self, existing_time_step, new_step): notes_in_step = existing_time_step.filter_notes(self._clip_notes) new_end = float(self.get_step_start_time(new_step) + self._get_step_length()) step_mute = all(map(lambda note: note_muted(note), notes_in_step)) new_notes = [] for note in self._clip_notes: length_offset = new_end - (note_length(note) + note_start_time(note)) if note in notes_in_step else 0 new_notes.append(self._modify_single_note(step_mute, existing_time_step, length_offset, note)) self._replace_notes(tuple(new_notes)) def _time_step(self, time): return TimeStep(time, self._get_step_length()) def _get_notes_info_from_step(self, step): time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) pitches = [ note_pitch(note) for note in notes ] return ( time, notes, pitches) def toggle_pitch_for_all_modified_steps(self, pitch): assert liveobj_valid(self._sequencer_clip) for step in set(chain(self._pressed_steps, self._modified_steps)): time, notes, pitches = self._get_notes_info_from_step(step) if pitch not in pitches: self._add_new_note_in_step(step, pitch, time) else: time_step = self._time_step(self.get_step_start_time(step)) for time, length in time_step.connected_time_ranges(): remove_single_note(self._sequencer_clip, time, (pitch,), length) def _add_new_note_in_step(self, step, pitch, time): mute = self.mute_button.is_pressed velocity = 127 if self.full_velocity else self._velocity_provider.velocity note = (pitch, time, self._get_step_length(), velocity, mute) self._sequencer_clip.set_notes((note,)) self._sequencer_clip.deselect_all_notes() self._trigger_modification(step, done=True) def _add_or_modify_note_in_step(self, step, pitch, modify_existing=True): u""" Add note in given step if there are none in there, otherwise select the step for potential deletion or modification """ if liveobj_valid(self._sequencer_clip): time, notes, pitches = self._get_notes_info_from_step(step) if notes: if pitch in pitches and modify_existing: most_significant_velocity = most_significant_note(notes)[3] if self.mute_button.is_pressed or most_significant_velocity != 127 and self.full_velocity: self._trigger_modification(step, immediate=True) else: self._add_new_note_in_step(step, pitch, time) return True return False def _delete_notes_in_step(self, step): u""" Delete all notes in the given step """ if liveobj_valid(self._sequencer_clip) and self._can_edit(): time_step = self._time_step(self.get_step_start_time(step)) for time, length in time_step.connected_time_ranges(): self._remove_notes_handler(self._sequencer_clip, time, self._pitches, length) def set_nudge_offset(self, value): self._modify_note_property('_nudge_offset', value) def set_length_offset(self, value): self._modify_note_property('_length_offset', value) def set_velocity_offset(self, value): self._modify_note_property('_velocity_offset', value) def _modify_note_property(self, note_property, value): if self.is_enabled(): with self._full_velocity_context(False): setattr(self, note_property, getattr(self, note_property) + value) self._trigger_modification(immediate=True) @contextmanager def _full_velocity_context(self, desired_full_velocity_state): saved_velocity = self.full_velocity self.full_velocity = desired_full_velocity_state yield self.full_velocity = saved_velocity def set_full_velocity(self): if self.is_enabled(): with self._full_velocity_context(True): self._trigger_modification() def notify_modification(self): u""" Tell the note editor about changes to pressed steps, so further modifications by the note editor are ignored. """ self._trigger_modification(done=True) def _trigger_modification(self, step=None, done=False, immediate=False): u""" Because the modification of notes is slow, we accumulate modification events and perform all of them alltogether in a task. Call this function whenever a given set of steps (or potentially all steps) are to be modified. If done=True, we just notify that the given steps have been modified without actually executing the task. """ needs_update = False if step is None: needs_update = bool(self._pressed_steps) self._modified_steps += self._pressed_steps self._pressed_steps = [] elif step not in self._modified_steps: self._modified_steps.append(step) else: if step in self._pressed_steps: self._pressed_steps.remove(step) needs_update = True if not done: if immediate: self._do_modification() self._modify_task.kill() elif self._modify_task.is_killed: self._do_modification() self._modify_task.restart() if needs_update: self._update_editor_matrix() return def _reset_modifications(self): self._velocity_offset = 0 self._length_offset = 0 self._nudge_offset = 0 def _do_modification(self): if self._modify_all_notes_enabled: new_notes = self._modify_all_notes() self._replace_notes(new_notes) elif self._modified_steps: notes_added = map(lambda step_and_pitch: self._add_or_modify_note_in_step(modify_existing=False, *step_and_pitch), product(self._modified_steps, self._pitches)) if any(notes_added): self._modify_task.restart() else: new_notes = self._modify_step_notes(self._modified_steps) self._replace_notes(new_notes) self._reset_modifications() def _replace_notes(self, new_notes): if new_notes != self._clip_notes and self._can_edit(): time_start, time_length = self._get_clip_notes_time_range() self._remove_notes_handler(self._sequencer_clip, time_start, self._pitches, time_length) self._sequencer_clip.set_notes(tuple(new_notes)) self._sequencer_clip.deselect_all_notes() def _modify_all_notes(self): u""" modify all notes in the current pitch """ return self._modify_notes_in_time(TimeStep(0.0, MAX_CLIP_LENGTH), self._clip_notes, self._length_offset) def _limited_nudge_offset(self, steps, notes, nudge_offset): limited_nudge_offset = MAX_CLIP_LENGTH for step in steps: time_step = self._time_step(self.get_step_start_time(step)) for note in time_step.filter_notes(notes): start_time = note_start_time(note) time_after_nudge = time_step.clamp(start_time, nudge_offset) limited_nudge_offset = min(limited_nudge_offset, abs(start_time - time_after_nudge)) return sign(nudge_offset) * limited_nudge_offset def _modify_step_notes(self, steps): u""" Return a new list with all notes within steps modified. """ notes = self._clip_notes self._nudge_offset = self._limited_nudge_offset(steps, notes, self._nudge_offset) for step in steps: time_step = self._time_step(self.get_step_start_time(step)) notes = self._modify_notes_in_time(time_step, notes, self._length_offset) return notes def _modify_notes_in_time(self, time_step, notes, length_offset): step_notes = time_step.filter_notes(self._clip_notes) step_mute = all(map(lambda note: note_muted(note), step_notes)) return map(partial(self._modify_single_note, step_mute, time_step, length_offset), notes) def _modify_single_note(self, step_mute, time_step, length_offset, note): u""" Return a modified version of the passed in note taking into account current modifiers. If the note is not within the given step, returns the note as-is. If the time_step is inside a loop, the last part of the loop is considered as being the same as the part just before the loop, so the resulting note may, in this case, jump between the beginning and the end. """ pitch, time, length, velocity, mute = note if time_step.includes_time(time): time = time_step.clamp(time, self._nudge_offset) if length_offset <= -time_step.length and length + length_offset < time_step.length: if length > time_step.length: length = time_step.length else: length = max(0, length + length_offset) if self._provided_velocity: velocity = self._provided_velocity elif self.full_velocity: velocity = 127 else: velocity = clamp(velocity + self._velocity_offset, 1, 127) mute = not step_mute if self.mute_button.is_pressed else mute return ( pitch, time, length, velocity, mute) def get_min_max_note_values(self): if self._modify_all_notes_enabled and len(self._clip_notes) > 0: return min_max_for_notes(self._clip_notes, 0.0) else: if len(self._pressed_steps) + len(self._modified_steps) > 0: min_max_values = None for step in chain(self._modified_steps, self._pressed_steps): start_time = self.get_step_start_time(step) min_max_values = min_max_for_notes(self._time_step(start_time).filter_notes(self._clip_notes), start_time, min_max_values) return min_max_values return
class DrumGroupComponent(ResettableSlideComponent, Slideable): __events__ = (u'pressed_pads', ) mute_button = ButtonControl() solo_button = ButtonControl() delete_button = ButtonControl(**ACTION_BUTTON_COLORS) quantize_button = ButtonControl() select_button = ButtonControl(color=u'Misc.Shift', pressed_color=u'Misc.ShiftOn') drum_matrix = control_matrix(PlayableControl) @depends(set_pad_translations=None) def __init__(self, pitch_deleter, translation_channel=None, set_pad_translations=None, *a, **k): self._pitch_deleter = pitch_deleter self._takeover_drums = False self._drum_group_device = None self._selected_drum_pad = None self._all_drum_pads = [] self._selected_pads = [] self._visible_drum_pads = [] self._translation_channel = translation_channel self._coordinate_to_pad_map = {} super(DrumGroupComponent, self).__init__(*a, **k) self._set_pad_translations = set_pad_translations self._on_selected_clip_changed.subject = self._pitch_deleter self._layout_set = False position_count = 32 page_length = 4 page_offset = 1 def contents_range(self, pmin, pmax): pos_count = self.position_count first_pos = max(int(pmin - 0.05), 0) last_pos = min(int(pmax + 0.2), pos_count) return xrange(first_pos, last_pos) def contents(self, index): drum = self._drum_group_device if drum: return any( imap(lambda pad: pad.chains, drum.drum_pads[index * 4:index * 4 + 4])) return False def _get_position(self): if self._drum_group_device: return self._drum_group_device.view.drum_pads_scroll_position return 0 def _set_position(self, index): if not 0 <= index <= 28: raise AssertionError self._drum_group_device.view.drum_pads_scroll_position = self._drum_group_device and index position = property(_get_position, _set_position) @property def width(self): if self.drum_matrix.width: return self.drum_matrix.width return 4 @property def height(self): if self.drum_matrix.height: return self.drum_matrix.height return 4 @property def pressed_pads(self): return self._selected_pads @property def visible_drum_pads(self): if self._visible_drum_pads and self._all_drum_pads: first_pad = first(self._visible_drum_pads) if first_pad: size = self.width * self.height first_note = first_pad.note if first_note > 128 - size: size = 128 - first_note offset = clamp(first_note, 0, 128 - len(self._visible_drum_pads)) return self._all_drum_pads[offset:offset + size] return [] def update(self): super(DrumGroupComponent, self).update() self._set_control_pads_from_script(False) self._update_led_feedback() def set_drum_matrix(self, matrix): if not matrix or not self._layout_set: self.drum_matrix.set_control_element(matrix) for button in self.drum_matrix: button.channel = self._translation_channel if self._selected_pads: self._selected_pads = [] self.notify_pressed_pads() self._create_and_set_pad_translations() self._update_control_from_script() self._update_identifier_translations() self._layout_set = bool(matrix) self._update_led_feedback() @listens(u'selected_clip') def _on_selected_clip_changed(self): if self.is_enabled(): self.delete_button.enabled = self._pitch_deleter.can_perform_midi_clip_action( ) def set_drum_group_device(self, drum_group_device): if drum_group_device and not drum_group_device.can_have_drum_pads: drum_group_device = None if drum_group_device != self._drum_group_device: self._on_visible_drum_pads_changed.subject = drum_group_device drum_group_view = drum_group_device.view if drum_group_device else None self._on_selected_drum_pad_changed.subject = drum_group_view self._on_drum_pads_scroll_position_changed.subject = drum_group_view self._drum_group_device = drum_group_device self._update_drum_pad_listeners() self._on_selected_drum_pad_changed() self._update_identifier_translations() super(DrumGroupComponent, self).update() def _update_drum_pad_listeners(self): u""" add and remove listeners for visible drum pads, including mute and solo state """ if self._drum_group_device: self._all_drum_pads = self._drum_group_device.drum_pads self._visible_drum_pads = self._drum_group_device.visible_drum_pads self._on_solo_changed.replace_subjects(self._visible_drum_pads) self._on_mute_changed.replace_subjects(self._visible_drum_pads) self._update_identifier_translations() @listens_group(u'solo') def _on_solo_changed(self, pad): self._update_led_feedback() @listens_group(u'mute') def _on_mute_changed(self, pad): self._update_led_feedback() def _update_led_feedback(self): if self._drum_group_device: soloed_pads = find_if(lambda pad: pad.solo, self._all_drum_pads) for button in self.drum_matrix: pad = self._coordinate_to_pad_map.get(button.coordinate, None) if pad: self._update_pad_led(pad, button, soloed_pads) def _update_pad_led(self, pad, button, soloed_pads): button_color = u'DrumGroup.PadEmpty' if pad == self._selected_drum_pad: if soloed_pads and not pad.solo and not pad.mute: button_color = u'DrumGroup.PadSelectedNotSoloed' elif pad.mute and not pad.solo: button_color = u'DrumGroup.PadMutedSelected' elif soloed_pads and pad.solo: button_color = u'DrumGroup.PadSoloedSelected' else: button_color = u'DrumGroup.PadSelected' elif pad.chains: if soloed_pads and not pad.solo: if not pad.mute: button_color = u'DrumGroup.PadFilled' else: button_color = u'DrumGroup.PadMuted' elif not soloed_pads and pad.mute: button_color = u'DrumGroup.PadMuted' elif soloed_pads and pad.solo: button_color = u'DrumGroup.PadSoloed' else: button_color = u'DrumGroup.PadFilled' else: button_color = u'DrumGroup.PadEmpty' button.color = button_color def _button_coordinates_to_pad_index(self, first_note, coordinates): y, x = coordinates y = self.height - y - 1 if x < 4 and y >= 4: first_note += 16 elif x >= 4 and y < 4: first_note += 4 * self.width elif x >= 4 and y >= 4: first_note += 4 * self.width + 16 index = x % 4 + y % 4 * 4 + first_note return index @drum_matrix.pressed def drum_matrix(self, pad): self._on_matrix_pressed(pad) @drum_matrix.released def drum_matrix(self, pad): self._on_matrix_released(pad) def _on_matrix_released(self, pad): selected_drum_pad = self._coordinate_to_pad_map[pad.coordinate] if selected_drum_pad in self._selected_pads: self._selected_pads.remove(selected_drum_pad) if not self._selected_pads: self._update_control_from_script() self.notify_pressed_pads() self._update_led_feedback() def _on_matrix_pressed(self, pad): selected_drum_pad = self._coordinate_to_pad_map[pad.coordinate] if self.mute_button.is_pressed: selected_drum_pad.mute = not selected_drum_pad.mute if self.solo_button.is_pressed: selected_drum_pad.solo = not selected_drum_pad.solo if self.quantize_button.is_pressed: pad.color = u'DrumGroup.PadAction' self.quantize_pitch(selected_drum_pad.note) if self.delete_button.is_pressed: pad.color = u'DrumGroup.PadAction' self.delete_pitch(selected_drum_pad) if self.select_button.is_pressed: self._drum_group_device.view.selected_drum_pad = selected_drum_pad self.select_drum_pad(selected_drum_pad) self._selected_pads.append(selected_drum_pad) if len(self._selected_pads) == 1: self._update_control_from_script() self.notify_pressed_pads() if self.mute_button.is_pressed or self.solo_button.is_pressed: self._update_led_feedback() @listens(u'visible_drum_pads') def _on_visible_drum_pads_changed(self): self._update_drum_pad_listeners() self._update_led_feedback() @listens(u'drum_pads_scroll_position') def _on_drum_pads_scroll_position_changed(self): self._update_identifier_translations() self._update_led_feedback() self.notify_position() @listens(u'selected_drum_pad') def _on_selected_drum_pad_changed(self): self._selected_drum_pad = self._drum_group_device.view.selected_drum_pad if self._drum_group_device else None self._update_led_feedback() @mute_button.value def mute_button(self, value, button): self._set_control_pads_from_script(bool(value)) @solo_button.value def solo_button(self, value, button): self._set_control_pads_from_script(bool(value)) @delete_button.value def delete_button(self, value, button): self._set_control_pads_from_script(bool(value)) @quantize_button.value def quantize_button(self, value, button): self._set_control_pads_from_script(bool(value)) @select_button.value def select_button(self, value, button): self._set_control_pads_from_script(bool(value)) def _set_control_pads_from_script(self, takeover_drums): u""" If takeover_drums, the matrix buttons will be controlled from the script. Otherwise they send midi notes to the track associated to this drum group. """ if takeover_drums != self._takeover_drums: self._takeover_drums = takeover_drums self._update_control_from_script() def _update_control_from_script(self): takeover_drums = self._takeover_drums or bool(self._selected_pads) for button in self.drum_matrix: button.set_playable(not takeover_drums) def _update_identifier_translations(self): if not self._can_set_pad_translations(): self._set_non_pad_translated_identifiers() else: self._set_pad_translated_identifiers() def _set_non_pad_translated_identifiers(self): visible_drum_pads = self.visible_drum_pads if visible_drum_pads: for button in self.drum_matrix: identifier = self._button_coordinates_to_pad_index( first(visible_drum_pads).note, button.coordinate) if identifier < 128: button.identifier = identifier button.enabled = True self._coordinate_to_pad_map[ button.coordinate] = self._all_drum_pads[ button.identifier] else: button.enabled = False def _set_pad_translated_identifiers(self): visible_drum_pads = self.visible_drum_pads if visible_drum_pads: for index, button in enumerate(self.drum_matrix): row, col = button.coordinate self._coordinate_to_pad_map[self.width - 1 - row, col] = visible_drum_pads[index] def _can_set_pad_translations(self): return self.width <= 4 and self.height <= 4 def _create_and_set_pad_translations(self): def create_translation_entry(button): row, col = button.coordinate button.identifier = self._button_coordinates_to_pad_index( BASE_DRUM_RACK_NOTE, button.coordinate) return (col, row, button.identifier, button.channel) if self._can_set_pad_translations(): translations = tuple( map(create_translation_entry, self.drum_matrix)) self._set_pad_translated_identifiers() else: translations = None self._set_non_pad_translated_identifiers() self._set_pad_translations(translations) def select_drum_pad(self, drum_pad): u""" Override when you give it a select button """ pass def quantize_pitch(self, note): u""" Override when you give it a quantize button """ raise NotImplementedError def delete_pitch(self, drum_pad): self._pitch_deleter.delete_pitch(drum_pad.note)
class MonoKeyGroupComponent(PlayableComponent): _scales = SCALES _scale = DEFAULT_AUTO_SCALE _vertoffset = DEFAULT_VERTOFFSET _offset = DEFAULT_OFFSET _translation_channel = 0 _selected_note = 0 _selected_notes = tuple([0]) select_matrix = control_matrix(PlayableControl) def __init__(self, channel_list=CHANNELS, settings=DEFAULT_INSTRUMENT_SETTINGS, *a, **k): self._channel_list = channel_list self._settings = settings self._scales = settings['Scales'] super(MonoKeyGroupComponent, self).__init__(*a, **k) self.selected_notes_provider = self self.select_button._send_current_color = lambda: None self._on_selected_track_changed.subject = self.song.view def _get_current_channel(self): cur_track = self.song.view.selected_track cur_chan = cur_track.current_input_sub_routing if len(cur_chan) == 0: cur_chan = 'All Channels' if cur_chan == 'All Channels': cur_chan = 1 if cur_chan in self._channel_list: cur_chan = (self._channel_list.index(cur_chan) % 15) + 1 else: cur_chan = 14 return cur_chan @listens('selected_track') def _on_selected_track_changed(self): self.translation_channel = self._get_current_channel() self.update_matrix() @property def translation_channel(self, translation_channel): return self._translation_channel @translation_channel.setter def translation_channel(self, channel): self._translation_channel = channel self.update_matrix() @property def scale(self): return self._scale @scale.setter def scale(self, scale): self._scale = scale self.update_matrix() @property def vertical_offset(self): return self._vertoffset @vertical_offset.setter def vertical_offset(self, offset): self._vertoffset = offset self.update_matrix() @property def offset(self): return self._offset @offset.setter def offset(self, offset): self._offset = offset self.update_matrix() def update_matrix(self): self._reset_selected_pads() self._update_led_feedback() self._update_note_translations() def _update_led_feedback(self): super(MonoKeyGroupComponent, self)._update_led_feedback() for button in self.select_matrix: self._update_button_color(button) def set_select_matrix(self, matrix): self.select_matrix.set_control_element(matrix) for button in self.select_matrix: #button.set_playable(False) button.set_mode(PlayableControl.Mode.listenable) self._update_led_feedback() def _note_translation_for_button(self, button): y, x = button.coordinate scale_len = len(self._scales[self._scale]) note_pos = x + (abs((self.height - 1) - y) * self._vertoffset) note = self._offset + self._scales[self._scale][ note_pos % scale_len] + (12 * int(note_pos / scale_len)) return (note, self._translation_channel) def set_matrix(self, matrix): self._set_matrix_special_attributes(False) super(MonoKeyGroupComponent, self).set_matrix(matrix) self._set_matrix_special_attributes(True) def _set_matrix_special_attributes(self, enabled): for button in self.matrix: if button._control_element: button._control_element.display_press = enabled button._control_element._last_flash = 0 not enabled and button._control_element.reset_state() def _update_button_color(self, button): y, x = button.coordinate scale_len = len(self._scales[self._scale]) note_pos = x + (abs((self.height - 1) - y) * self._vertoffset) note = self._offset + self._scales[self._scale][ note_pos % scale_len] + (12 * int(note_pos / scale_len)) if note is self.selected_note: button.color = SELECTED_NOTE else: button.color = KEYCOLORS[(note % 12 in WHITEKEYS) + (((note_pos % scale_len) == 0) * 2)] if button._control_element: button._control_element.scale_color = button.color @select_matrix.pressed def select_matrix(self, button): self._on_matrix_pressed(button) def _on_matrix_pressed(self, button): super(MonoKeyGroupComponent, self)._on_matrix_pressed(button) self._selected_note = self._note_translation_for_button(button)[0] self.notify_selected_note() self.selected_notes = [int(self._selected_note)] self._update_led_feedback() @listenable_property def selected_note(self): return int(self._selected_note) @listenable_property def selected_notes(self): return tuple([int(self._selected_note)]) @selected_notes.setter def selected_notes(self, notes): self._selected_notes = tuple(notes) self.notify_selected_notes(self._selected_notes)
class NoteEditorComponent(Component): __events__ = ('page_length', 'active_note_regions', 'active_steps', 'notes_changed', 'modify_all_notes') matrix = control_matrix(PadControl, channel=(PLAYHEAD_FEEDBACK_CHANNELS[0]), sensitivity_profile='loop', mode=(PlayableControl.Mode.listenable)) mute_button = ButtonControl(color='DefaultButton.Transparent') def __init__(self, clip_creator=None, grid_resolution=None, skin_base_key='NoteEditor', velocity_range_thresholds=None, velocity_provider=None, get_notes_handler=get_single_note, remove_notes_handler=remove_single_note, duplicate_all_notes=False, *a, **k): (super(NoteEditorComponent, self).__init__)(*a, **k) self._duplicate_all_notes = duplicate_all_notes self._get_notes_handler = get_notes_handler self._remove_notes_handler = remove_notes_handler self._skin_base_key = skin_base_key self.full_velocity = False self._provided_velocity = None self._selected_page_point = 0 self._page_index = 0 self._clip_creator = clip_creator self._sequencer_clip = None self._step_colors = [] self._pressed_steps = [] self._modified_steps = [] self._pressed_step_callback = None self._modify_task = self._tasks.add(task.run(self._do_modification)) self._modify_task.kill() self._modify_all_notes_enabled = False self._step_tap_tasks = {} self._clip_notes = [] self._pitches = [DEFAULT_START_NOTE] self._grid_resolution = grid_resolution self._NoteEditorComponent__on_resolution_changed.subject = self._grid_resolution self.set_step_duplicator(None) self._nudge_offset = 0 self._length_offset = 0 self._velocity_offset = 0 self._velocity_deviation_offset = 0 self._probability_offset = 0 self._triplet_factor = 1.0 self._update_from_grid() self.background_color = self._skin_base_key + '.StepEmpty' self._velocity_range_thresholds = velocity_range_thresholds or DEFAULT_VELOCITY_RANGE_THRESHOLDS self._velocity_provider = velocity_provider or NullVelocityProvider() self._NoteEditorComponent__on_provided_velocity_changed.subject = self._velocity_provider @property def page_index(self): return self._page_index @property def page_length(self): return self._get_step_count() * self._get_step_length( ) * self._triplet_factor @property def can_change_page(self): return not self._pressed_steps and not self._modified_steps @property def notes_in_selected_step(self): time_steps = [] if self._modify_all_notes_enabled: time_steps = [TimeStep(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)] else: time_steps = [ self._time_step(self.get_step_start_time(start_time)) for start_time in chain(self._pressed_steps, self._modified_steps) ] time_steps_with_notes = [ time_step.filter_notes(self._clip_notes) for time_step in time_steps ] notes_in_step = [] for time_step in time_steps_with_notes: for note in time_step: notes_in_step.append(note.pitch) return notes_in_step def set_selected_page_point(self, point): self._selected_page_point = point index = int(old_div(point, self.page_length)) if self.page_length != 0 else 0 if index != self._page_index: self._page_index = index self._on_clip_notes_changed() def _get_modify_all_notes_enabled(self): return self._modify_all_notes_enabled def _set_modify_all_notes_enabled(self, enabled): if enabled != self._modify_all_notes_enabled: self._modify_all_notes_enabled = enabled self._on_clip_notes_changed() self.notify_modify_all_notes() modify_all_notes_enabled = property(_get_modify_all_notes_enabled, _set_modify_all_notes_enabled) def set_detail_clip(self, clip): if liveobj_changed(clip, self._sequencer_clip): self._sequencer_clip = clip self._step_duplicator.set_clip(clip) if self.can_change_page: self.set_selected_page_point(0) self._on_clip_notes_changed.subject = clip self._on_clip_notes_changed() def _can_edit(self): return len(self._pitches) != 0 def _get_editing_notes(self): return self._pitches def _set_editing_notes(self, pitches): self._pitches = pitches enabled = self._can_edit() for button in self.matrix: button.enabled = enabled if enabled: self._on_clip_notes_changed() editing_notes = property(_get_editing_notes, _set_editing_notes) def _get_width(self): if self.matrix.width: return self.matrix.width return 4 def _get_height(self): if self.matrix.height: return self.matrix.height return 4 def set_matrix(self, matrix): last_page_length = self.page_length self.matrix.set_control_element(matrix) for t in self._step_tap_tasks.values(): t.kill() def trigger_modification_task(x, y): trigger = partial((self._trigger_modification), (x, y), done=True) return self._tasks.add( task.sequence(task.wait(defaults.MOMENTARY_DELAY), task.run(trigger))).kill() self._step_tap_tasks = dict([ ((x, y), trigger_modification_task(x, y)) for x, y in product( range(self._get_width()), range(self._get_height())) ]) if matrix and last_page_length != self.page_length: self._on_clip_notes_changed() self.notify_page_length() else: self._update_editor_matrix() def set_step_duplicator(self, duplicator): self._step_duplicator = duplicator or NullStepDuplicator() self._step_duplicator.set_clip(self._sequencer_clip) def update(self): super(NoteEditorComponent, self).update() self._update_editor_matrix_leds() def _get_clip_notes_time_range(self): if self._modify_all_notes_enabled: time_length = self._get_step_count() * 4.0 time_start = 0 else: time_length = self.page_length time_start = self._page_index * time_length return (time_start - self._time_step(0).offset, time_length) @listens('notes') def _on_clip_notes_changed(self): if liveobj_valid(self._sequencer_clip) and self._can_edit(): time_start, time_length = self._get_clip_notes_time_range() self._clip_notes = self._get_notes_handler(self._sequencer_clip, time_start, self._pitches, time_length) else: self._clip_notes = [] self._update_editor_matrix() self.notify_notes_changed() def _update_editor_matrix(self): step_colors = [self._skin_base_key + '.StepDisabled' ] * self._get_step_count() def coords_to_index(coord): return coord[0] + coord[1] * self._get_width() editing_indices = set(map(coords_to_index, self._modified_steps)) selected_indices = set(map(coords_to_index, self._pressed_steps)) last_editing_notes = [] for time_step, index in self._visible_steps(): notes = time_step.filter_notes(self._clip_notes) if len(notes) > 0: last_editing_notes = [] if index in selected_indices: color = self._skin_base_key + '.StepSelected' elif index in editing_indices: note_color = self._determine_color(notes) color = self._skin_base_key + '.StepEditing.' + note_color last_editing_notes = notes else: note_color = self._determine_color(notes) color = self._skin_base_key + '.Step.' + note_color elif any(map(time_step.overlaps_note, last_editing_notes)): color = self._skin_base_key + '.StepEditing.' + note_color elif index in editing_indices or index in selected_indices: color = self._skin_base_key + '.StepSelected' last_editing_notes = [] else: color = self.background_color last_editing_notes = [] step_colors[index] = color self._step_colors = step_colors self._update_editor_matrix_leds() def _determine_color(self, notes): return color_for_note( (most_significant_note(notes)), velocity_range_thresholds=(self._velocity_range_thresholds)) def _visible_steps(self): first_time = self.page_length * self._page_index steps_per_page = self._get_step_count() step_length = self._get_step_length() indices = list(range(steps_per_page)) if is_triplet_quantization(self._triplet_factor): indices = [k for k in indices if k % 8 not in (6, 7)] return [(self._time_step(first_time + k * step_length), index) for k, index in enumerate(indices)] def _update_editor_matrix_leds(self): if self.is_enabled(): for control in self.matrix: control.color = self._step_colors[control.index] def _get_step_count(self): return self._get_width() * self._get_height() def get_step_start_time(self, step): x, y = step page_time = self._page_index * self._get_step_count( ) * self._triplet_factor step_time = x + y * self._get_width() * self._triplet_factor return (page_time + step_time) * self._get_step_length() def _get_step_length(self): return self._grid_resolution.step_length def get_row_start_times(self): return [ self.get_step_start_time((0, row)) for row in range(self._get_height()) ] def _update_from_grid(self): quantization, is_triplet = self._grid_resolution.clip_grid self._triplet_factor = 1.0 if (not is_triplet) else 0.75 if self._clip_creator: self._clip_creator.grid_quantization = quantization self._clip_creator.is_grid_triplet = is_triplet if liveobj_valid(self._sequencer_clip): self._sequencer_clip.view.grid_quantization = quantization self._sequencer_clip.view.grid_is_triplet = is_triplet @mute_button.pressed def mute_button(self, button): self._trigger_modification(immediate=True) @listens('index') def __on_resolution_changed(self, *a): self._release_active_steps() self._update_from_grid() self.set_selected_page_point(self._selected_page_point) self.notify_page_length() self._on_clip_notes_changed() @matrix.pressed def matrix(self, button): self._on_pad_pressed(button.coordinate) @matrix.released def matrix(self, button): self._on_pad_released(button.coordinate) def _on_pad_pressed(self, coordinate): y, x = coordinate if self.is_enabled(): if not liveobj_valid(self._sequencer_clip): self.set_detail_clip( create_clip_in_selected_slot(self._clip_creator, self.song)) if self._can_press_or_release_step(x, y): self._on_press_step((x, y)) self._update_editor_matrix() def _on_pad_released(self, coordinate): y, x = coordinate if self.is_enabled(): if self._can_press_or_release_step(x, y): should_delete_first = not self.mute_button.is_pressed and not self.full_velocity self._on_release_step((x, y), do_delete_notes=should_delete_first) self._update_editor_matrix() def _can_press_or_release_step(self, x, y): width = self._get_width( ) * self._triplet_factor if is_triplet_quantization( self._triplet_factor) else self._get_width() return x < width and y < self._get_height() @listens('velocity') def __on_provided_velocity_changed(self): if len(self._pressed_steps) + len(self._modified_steps) > 0: self._provided_velocity = self._velocity_provider.velocity self._trigger_modification(immediate=True) self._provided_velocity = None def _get_step_time_range(self, step): time = self.get_step_start_time(step) return (time, time + self._get_step_length()) @property def editing_note_regions(self): active_time_spans = self.active_time_spans editing_notes = set(self.notes_in_selected_step) if len(editing_notes) > 1: editing_notes = [-1] return list(product(editing_notes, list(active_time_spans))) @property def active_time_spans(self): if self._modify_all_notes_enabled: return [(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)] def time_span(step): time_step = self._time_step(self.get_step_start_time(step)) return (time_step.left_boundary(), time_step.right_boundary()) return list( map(time_span, chain(self._pressed_steps, self._modified_steps))) @property def active_steps(self): return list( map(self._get_step_time_range, chain(self._pressed_steps, self._modified_steps))) @property def active_note_regions(self): return list( map(self._get_time_range, chain(self._pressed_steps, self._modified_steps))) def _get_time_range(self, step): def note_compare(left, right): return left.start_time - right.start_time time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) if notes: beginning_note = first( sorted(notes, key=(cmp_to_key(note_compare)))) start = beginning_note.start_time end = start + beginning_note.duration if len(notes) > 1: end_note = notes[(-1)] end = end_note.start_time + end_note.duration return (start, end) return (time, time + self._get_step_length()) def _release_active_steps(self): for step in self._pressed_steps + self._modified_steps: self._on_release_step(step, do_delete_notes=False) def _on_release_step(self, step, do_delete_notes=True): self._step_tap_tasks[step].kill() if step in self._pressed_steps: if do_delete_notes: self._delete_notes_in_step(step) self._pressed_steps.remove(step) for pitch in self._pitches: self._add_or_modify_note_in_step(step, pitch) if step in self._modified_steps: self._modified_steps.remove(step) if len(self._modified_steps) + len(self._pressed_steps) == 0: self._velocity_provider.set_velocities_playable(True) self.notify_active_steps() self.notify_active_note_regions() def _find_continued_step(self, step): def steps_to_note_start(steps): return [self.get_step_start_time(step) for step in steps] time = self.get_step_start_time(step) all_steps_with_notes = [ time_step for time_step, index in self._visible_steps() if time_step.filter_notes(self._clip_notes) ] all_time_step_starts = [ts.start for ts in all_steps_with_notes] if all_time_step_starts: insert_point = bisect(all_time_step_starts, time) if insert_point > 0: prev_filled_step = all_steps_with_notes[(insert_point - 1)] notes_in_modified_steps = steps_to_note_start( self._modified_steps) if prev_filled_step.start in notes_in_modified_steps: return prev_filled_step def _add_step_to_duplicator(self, step): nudge_offset = 0 time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) step_start, step_end = self._get_time_range(step) if notes: nudge_offset = min(map(lambda n: n.start_time, notes)) - time if self._duplicate_all_notes: self._step_duplicator.add_step(step_start, step_end, nudge_offset) else: self._step_duplicator.add_step_with_pitch(self.editing_notes[0], step_start, step_end, nudge_offset) def _on_press_step(self, step): if liveobj_valid(self._sequencer_clip): if step not in self._pressed_steps: if step not in self._modified_steps: if self._step_duplicator.is_duplicating: self._add_step_to_duplicator(step) else: self._step_tap_tasks[step].restart() continued_step = self._find_continued_step(step) if continued_step: self._modify_length_of_notes_within_existing_step( continued_step, step) else: self._pressed_steps.append(step) self._velocity_provider.set_velocities_playable(False) self.notify_active_steps() self.notify_active_note_regions() def _modify_length_of_notes_within_existing_step(self, existing_time_step, new_step): notes_in_step = existing_time_step.filter_notes(self._clip_notes) new_end = float( self.get_step_start_time(new_step) + self._get_step_length()) step_mute = all([note.mute for note in notes_in_step]) for note in self._clip_notes: length_offset = new_end - (note.duration + note.start_time ) if note in notes_in_step else 0 self._modify_single_note(step_mute, existing_time_step, length_offset, note) self._replace_notes() def _time_step(self, time): return TimeStep(time, self._get_step_length()) def _get_notes_info_from_step(self, step): time = self.get_step_start_time(step) notes = self._time_step(time).filter_notes(self._clip_notes) pitches = [note.pitch for note in notes] return (time, notes, pitches) def toggle_pitch_for_all_modified_steps(self, pitch): for step in set(chain(self._pressed_steps, self._modified_steps)): time, notes, pitches = self._get_notes_info_from_step(step) if pitch not in pitches: self._add_new_note_in_step(step, pitch, time) else: time_step = self._time_step(self.get_step_start_time(step)) for time, length in time_step.connected_time_ranges(): remove_single_note(self._sequencer_clip, time, (pitch, ), length) def _add_new_note_in_step(self, step, pitch, time): mute = self.mute_button.is_pressed velocity = 127 if self.full_velocity else self._velocity_provider.velocity note = Live.Clip.MidiNoteSpecification( pitch=pitch, start_time=time, duration=(self._get_step_length()), velocity=velocity, mute=mute) self._sequencer_clip.add_new_notes((note, )) self._sequencer_clip.deselect_all_notes() self._trigger_modification(step, done=True) def _add_or_modify_note_in_step(self, step, pitch, modify_existing=True): if liveobj_valid(self._sequencer_clip): time, notes, pitches = self._get_notes_info_from_step(step) if notes: if not pitch in pitches or modify_existing: most_significant_velocity = most_significant_note( notes).velocity if self.mute_button.is_pressed or ( most_significant_velocity != 127 and self.full_velocity): self._trigger_modification(step, immediate=True) if not most_significant_velocity == 127 or self.full_velocity: self._delete_notes_in_step(step) else: self._add_new_note_in_step(step, pitch, time) return True return False def _delete_notes_in_step(self, step): if liveobj_valid(self._sequencer_clip): if self._can_edit(): time_step = self._time_step(self.get_step_start_time(step)) for time, length in time_step.connected_time_ranges(): self._remove_notes_handler(self._sequencer_clip, time, self._pitches, length) def set_nudge_offset(self, value): self._modify_note_property('_nudge_offset', value) def set_length_offset(self, value): self._modify_note_property('_length_offset', value) def set_velocity_offset(self, value): self._modify_note_property('_velocity_offset', value) def set_velocity_deviation_offset(self, value): self._modify_note_property('_velocity_deviation_offset', value) def set_probability_offset(self, value): self._modify_note_property('_probability_offset', value) def _modify_note_property(self, note_property, value): if self.is_enabled(): with self._full_velocity_context(False): setattr(self, note_property, getattr(self, note_property) + value) self._trigger_modification(immediate=True) @contextmanager def _full_velocity_context(self, desired_full_velocity_state): saved_velocity = self.full_velocity self.full_velocity = desired_full_velocity_state (yield) self.full_velocity = saved_velocity def set_full_velocity(self): if self.is_enabled(): with self._full_velocity_context(True): self._trigger_modification() def notify_modification(self): self._trigger_modification(done=True) def _trigger_modification(self, step=None, done=False, immediate=False): needs_update = False if step is None: needs_update = bool(self._pressed_steps) self._modified_steps += self._pressed_steps self._pressed_steps = [] else: if step not in self._modified_steps: self._modified_steps.append(step) if step in self._pressed_steps: self._pressed_steps.remove(step) needs_update = True if not done: if immediate: self._do_modification() self._modify_task.kill() elif self._modify_task.is_killed: self._do_modification() self._modify_task.restart() if needs_update: self._update_editor_matrix() def _reset_modifications(self): self._velocity_offset = 0 self._velocity_deviation_offset = 0 self._length_offset = 0 self._nudge_offset = 0 self._probability_offset = 0 def _do_modification(self): if self._modify_all_notes_enabled: self._modify_all_notes() self._replace_notes() elif self._modified_steps: notes_added = list( map( lambda step_and_pitch: (self._add_or_modify_note_in_step) (*step_and_pitch, **{ 'modify_existing': False }), product(self._modified_steps, self._pitches))) if any(notes_added): self._modify_task.restart() else: self._modify_step_notes(self._modified_steps) self._replace_notes() self._reset_modifications() def _replace_notes(self): if self._can_edit(): time_start, time_length = self._get_clip_notes_time_range() self._sequencer_clip.apply_note_modifications(self._clip_notes) def _modify_all_notes(self): self._modify_notes_in_time(TimeStep(0.0, MAX_CLIP_LENGTH), self._clip_notes, self._length_offset) def _limited_nudge_offset(self, steps, notes, nudge_offset): limited_nudge_offset = MAX_CLIP_LENGTH for step in steps: time_step = self._time_step(self.get_step_start_time(step)) for note in time_step.filter_notes(notes): start_time = note.start_time time_after_nudge = time_step.clamp(start_time, nudge_offset) limited_nudge_offset = min(limited_nudge_offset, abs(start_time - time_after_nudge)) return sign(nudge_offset) * limited_nudge_offset def _modify_step_notes(self, steps): notes = self._clip_notes self._nudge_offset = self._limited_nudge_offset( steps, notes, self._nudge_offset) for step in steps: time_step = self._time_step(self.get_step_start_time(step)) self._modify_notes_in_time(time_step, notes, self._length_offset) def _modify_notes_in_time(self, time_step, notes, length_offset): step_notes = time_step.filter_notes(self._clip_notes) step_mute = all([note.mute for note in step_notes]) for note in notes: self._modify_single_note(step_mute, time_step, length_offset, note) def _modify_single_note(self, step_mute, time_step, length_offset, note): if time_step.includes_time(note.start_time): note.start_time = time_step.clamp(note.start_time, self._nudge_offset) if length_offset <= -time_step.length and note.duration + length_offset < time_step.length: if note.duration > time_step.length: note.duration = time_step.length else: note.duration = max(0, note.duration + length_offset) if self._provided_velocity: note.velocity = self._provided_velocity elif self.full_velocity: note.velocity = 127 else: note.velocity = clamp(note.velocity + self._velocity_offset, 1, 127) note.velocity_deviation = clamp( note.velocity_deviation + self._velocity_deviation_offset, -127, 127) note.probability = clamp( note.probability + self._probability_offset / 100.0, 0, 1) note.mute = not step_mute if self.mute_button.is_pressed else note.mute def get_min_max_note_values(self): if self._modify_all_notes_enabled: if len(self._clip_notes) > 0: return min_max_for_notes(self._clip_notes, 0.0) if len(self._pressed_steps) + len(self._modified_steps) > 0: min_max_values = None for step in chain(self._modified_steps, self._pressed_steps): start_time = self.get_step_start_time(step) min_max_values = min_max_for_notes( self._time_step(start_time).filter_notes(self._clip_notes), start_time, min_max_values) return min_max_values
class InstrumentComponent(PlayableComponent, CompoundComponent, Slideable, Messenger): u""" Class that sets up the button matrix as a piano, using different selectable layouts for the notes. """ __events__ = (u'pattern', ) matrix = control_matrix(PadControl) delete_button = ButtonControl() def __init__(self, note_layout=None, *a, **k): assert note_layout is not None super(InstrumentComponent, self).__init__(*a, **k) self._note_layout = note_layout self._first_note = self.page_length * 3 + self.page_offset self._last_page_length = self.page_length self._last_page_offset = self.page_offset self._detail_clip = None self._has_notes = [False] * 128 self._has_notes_pattern = self._get_pattern(0) self._aftertouch_control = None self._show_notifications = True self.__on_detail_clip_changed.subject = self.song.view self.__on_detail_clip_changed() self._slider = self.register_component(SlideComponent(self)) self._touch_slider = self.register_component( SlideableTouchStripComponent(self)) for event in (u'scale', u'root_note', u'is_in_key', u'is_fixed', u'is_horizontal', u'interval'): self.register_slot(self._note_layout, self._on_note_layout_changed, event) self._update_pattern() return @listens(u'detail_clip') def __on_detail_clip_changed(self): clip = self.song.view.detail_clip self.set_detail_clip( clip if liveobj_valid(clip) and clip.is_midi_clip else None) return def set_detail_clip(self, clip): if clip != self._detail_clip: self._detail_clip = clip self._on_clip_notes_changed.subject = clip self._on_loop_start_changed.subject = clip self._on_loop_end_changed.subject = clip self._on_clip_notes_changed() @listens(u'notes') def _on_clip_notes_changed(self): if self._detail_clip: self._has_notes = [False] * 128 loop_start = self._detail_clip.loop_start loop_length = self._detail_clip.loop_end - loop_start notes = self._detail_clip.get_notes(loop_start, 0, loop_length, 128) for note in notes: self._has_notes[note[0]] = True self.notify_contents() @listens(u'loop_start') def _on_loop_start_changed(self): self._on_loop_selection_changed() @listens(u'loop_end') def _on_loop_end_changed(self): self._on_loop_selection_changed() def _on_loop_selection_changed(self): self._on_clip_notes_changed() def contents(self, index): if self._detail_clip: note = self._has_notes_pattern[index].index if note is not None: return self._has_notes[note] return False else: return False @property def show_notifications(self): return self._show_notifications @show_notifications.setter def show_notifications(self, value): self._show_notifications = value @property def page_length(self): if self._note_layout.is_in_key: return len(self._note_layout.notes) return 12 @property def position_count(self): if not self._note_layout.is_in_key: return 139 else: offset = self.page_offset octaves = 11 if self._note_layout.notes[0] < 8 else 10 return offset + len(self._note_layout.notes) * octaves def _first_scale_note_offset(self): if not self._note_layout.is_in_key: return self._note_layout.notes[0] elif self._note_layout.notes[0] == 0: return 0 else: return len(self._note_layout.notes) - index_if( lambda n: n >= 12, self._note_layout.notes) @property def page_offset(self): if self._note_layout.is_fixed: return 0 return self._first_scale_note_offset() def _get_position(self): return self._first_note def _set_position(self, note): self._first_note = note self._update_pattern() self._update_matrix() self.notify_position() position = property(_get_position, _set_position) @property def min_pitch(self): return self.pattern[0].index @property def max_pitch(self): identifiers = [control.identifier for control in self.matrix] if len(identifiers) > 0: return max(identifiers) return 127 @property def pattern(self): return self._pattern @matrix.pressed def matrix(self, button): self._on_matrix_pressed(button) def _on_matrix_pressed(self, button): if self.delete_button.is_pressed: pitch = self._get_note_info_for_coordinate(button.coordinate).index if pitch and self._detail_clip: self._do_delete_pitch(pitch) @matrix.released def matrix(self, button): self._on_matrix_released(button) def _on_matrix_released(self, button): pass def _do_delete_pitch(self, pitch): clip = self._detail_clip if clip: note_name = pitch_index_to_string(pitch) loop_length = clip.loop_end - clip.loop_start clip.remove_notes(clip.loop_start, pitch, loop_length, 1) self.show_notification(consts.MessageBoxText.DELETE_NOTES % note_name) @delete_button.pressed def delete_button(self, value): self._set_control_pads_from_script(True) @delete_button.released def delete_button(self, value): self._set_control_pads_from_script(False) def set_note_strip(self, strip): self._touch_slider.set_scroll_strip(strip) def set_octave_strip(self, strip): self._touch_slider.set_page_strip(strip) def set_octave_up_button(self, button): self._slider.set_scroll_page_up_button(button) def set_octave_down_button(self, button): self._slider.set_scroll_page_down_button(button) def set_scale_up_button(self, button): self._slider.set_scroll_up_button(button) def set_scale_down_button(self, button): self._slider.set_scroll_down_button(button) def set_aftertouch_control(self, control): self._aftertouch_control = control self._update_aftertouch() def _align_first_note(self): self._first_note = self.page_offset + ( self._first_note - self._last_page_offset) * float( self.page_length) / float(self._last_page_length) if self._first_note >= self.position_count: self._first_note -= self.page_length self._last_page_length = self.page_length self._last_page_offset = self.page_offset def _on_note_layout_changed(self, _): self._update_scale() def show_pitch_range_notification(self): if self.is_enabled() and self.show_notifications: self.show_notification(u'Play {start_note} to {end_note}'.format( start_note=pitch_index_to_string( self.pattern.note(0, 0).index), end_note=pitch_index_to_string( self.pattern.note(self.width - 1, self.height - 1).index))) def _update_scale(self): self._align_first_note() self._update_pattern() self._update_matrix() self.notify_position_count() self.notify_position() self.notify_contents() def update(self): super(InstrumentComponent, self).update() if self.is_enabled(): self._update_matrix() self._update_aftertouch() def _update_pattern(self): self._pattern = self._get_pattern() self._has_notes_pattern = self._get_pattern(0) self.notify_pattern() def _invert_and_swap_coordinates(self, coordinates): return (coordinates[1], self.height - 1 - coordinates[0]) def _get_note_info_for_coordinate(self, coordinate): x, y = self._invert_and_swap_coordinates(coordinate) return self.pattern.note(x, y) def _update_button_color(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) button.color = u'Instrument.' + note_info.color def _button_should_be_enabled(self, button): return self._get_note_info_for_coordinate( button.coordinate).index != None def _note_translation_for_button(self, button): note_info = self._get_note_info_for_coordinate(button.coordinate) return (note_info.index, note_info.channel) def _set_button_control_properties(self, button): super(InstrumentComponent, self)._set_button_control_properties(button) button.sensitivity_profile = u'default' if self._takeover_pads else u'instrument' def _update_matrix(self): self._update_control_from_script() self._update_note_translations() self._update_led_feedback() def _get_pattern(self, first_note=None): if first_note is None: first_note = int(round(self._first_note)) interval = self._note_layout.interval notes = self._note_layout.notes width = None height = None octave = first_note / self.page_length offset = first_note % self.page_length - self._first_scale_note_offset( ) if interval in CUSTOM_PATTERNS: interval = 8 elif interval == None: if self._note_layout.is_in_key: interval = len(self._note_layout.notes) if self._note_layout.is_horizontal: width = interval + 1 else: height = interval + 1 else: interval = 8 elif not self._note_layout.is_in_key: interval = [0, 2, 4, 5, 7, 9, 10, 11][interval] if self._note_layout.is_horizontal: steps = [1, interval] origin = [offset, 0] else: steps = [interval, 1] origin = [0, offset] log.info( "_get_pattern(%r) interval=%r notes=%r pagelen=%r octave=%r, offset=%r is_fixed=%r nlinterval=%r steps=%r origin=%r base_note=%r is_in_key=%r width=%r height %r", first_note, interval, notes, self.page_length, octave, offset, self._note_layout.is_fixed, self._note_layout.interval, steps, origin, octave * 12, self._note_layout.is_in_key, width, height) custom_pattern = CUSTOM_PATTERNS.get(self._note_layout.interval) if custom_pattern: return custom_pattern.impl(first_note=first_note, steps=steps, scale=notes, origin=origin, octave=octave, is_diatonic=self._note_layout.is_in_key, is_absolute=self._note_layout.is_fixed, direction=custom_pattern.direction) else: return MelodicPattern( steps=steps, scale=notes, origin=origin, root_note=octave * 12, chromatic_mode=not self._note_layout.is_in_key, width=width, height=height) def _update_aftertouch(self): if self.is_enabled() and self._aftertouch_control != None: self._aftertouch_control.send_value(u'mono') return