Beispiel #1
0
class ModesComponent(ModesComponentBase):
    __events__ = ('mode_byte', )
    mode_selection_control = InputControl()

    @mode_selection_control.value
    def mode_selection_control(self, value, _):
        modes = self.modes
        if value < len(modes):
            self.selected_mode = modes[value]
            self.notify_mode_byte(value)
class TransportComponent(TransportComponentBase):
    tempo_control = InputControl()
    tempo_display = TextDisplayControl()
    play_button = ButtonControl()
    stop_button = ButtonControl()
    shift_button = ButtonControl()
    tui_metronome_button = ToggleButtonControl()
    metronome_color_control = ButtonControl()
    follow_song_button = ButtonControl()
    clip_trigger_quantization_control = SendReceiveValueControl()
    clip_trigger_quantization_button_row = control_list(
        RadioButtonControl, len(RADIO_BUTTON_GROUP_QUANTIZATION_VALUES))
    clip_trigger_quantization_color_controls = control_list(
        ColorSysexControl, len(RADIO_BUTTON_GROUP_QUANTIZATION_VALUES))
    jump_backward_button = ButtonControl()
    jump_forward_button = ButtonControl()
    loop_start_display = TextDisplayControl()
    loop_length_display = TextDisplayControl()
    arrangement_position_display = TextDisplayControl()
    arrangement_position_control = EncoderControl()
    loop_start_control = EncoderControl()
    loop_length_control = EncoderControl()
    tui_arrangement_record_button = ToggleButtonControl()

    def __init__(self, *a, **k):
        super(TransportComponent, self).__init__(*a, **k)
        song = self.song
        self._cached_num_beats_in_bar = num_beats_in_bar(song)
        self.__on_song_tempo_changed.subject = song
        self.__on_song_tempo_changed()
        self.__on_metronome_changed.subject = song
        self.__on_metronome_changed()
        self.__on_clip_trigger_quantization_changed.subject = song
        self.__on_clip_trigger_quantization_changed()
        self.__on_follow_song_changed.subject = song.view
        self.__on_follow_song_changed()
        self.__on_signature_numerator_changed.subject = song
        self.__on_signature_denominator_changed.subject = song
        self.__on_loop_start_changed.subject = song
        self.__on_loop_start_changed()
        self.__on_loop_length_changed.subject = song
        self.__on_loop_length_changed()
        self.__on_arrangement_position_changed.subject = song
        self.__on_arrangement_position_changed()
        self.__on_record_mode_changed.subject = song
        self.__on_record_mode_changed()

    def set_tempo_control(self, control):
        self.tempo_control.set_control_element(control)

    @listens(b'tempo')
    def __on_song_tempo_changed(self):
        self.tempo_display[0] = (b'{0:.2f}').format(self.song.tempo)

    @tempo_control.value
    def tempo_control(self, value, _):
        self.song.tempo = clamp(float((b'').join(imap(chr, value[2:]))),
                                TEMPO_MIN, TEMPO_MAX)

    @play_button.pressed
    def play_button(self, _):
        song = self.song
        song.is_playing = not song.is_playing

    @stop_button.pressed
    def stop_button(self, _):
        self.song.stop_playing()
        if self.shift_button.is_pressed:
            self.song.current_song_time = 0.0

    @tui_metronome_button.toggled
    def tui_metronome_button(self, toggled, _):
        self.song.metronome = toggled

    @follow_song_button.pressed
    def follow_song_button(self, _):
        view = self.song.view
        view.follow_song = not view.follow_song

    @clip_trigger_quantization_control.value
    def clip_trigger_quantization_control(self, value, _):
        if is_valid_launch_quantize_value(value):
            self.song.clip_trigger_quantization = value

    @clip_trigger_quantization_button_row.checked
    def clip_trigger_quantization_button_row(self, button):
        self.song.clip_trigger_quantization = RADIO_BUTTON_GROUP_QUANTIZATION_VALUES[
            button.index]

    def _apply_value_to_arrangement_property(self, property_name, value):
        factor = 0.25 if self.shift_button.is_pressed else 1.0
        delta = factor * sign(value)
        old_value = getattr(self.song, property_name)
        setattr(self.song, property_name, max(0.0, old_value + delta))

    @arrangement_position_control.value
    def arrangement_position_control(self, value, _):
        self._apply_value_to_arrangement_property(b'current_song_time', value)

    @loop_start_control.value
    def loop_start_control(self, value, _):
        self._apply_value_to_arrangement_property(b'loop_start', value)

    @loop_length_control.value
    def loop_length_control(self, value, _):
        self._apply_value_to_arrangement_property(b'loop_length', value)

    @jump_backward_button.pressed
    def jump_backward_button(self, _):
        self.song.jump_by(self._cached_num_beats_in_bar * -1)

    @jump_forward_button.pressed
    def jump_forward_button(self, _):
        self.song.jump_by(self._cached_num_beats_in_bar)

    @tui_arrangement_record_button.toggled
    def tui_arrangement_record_button(self, toggled, _):
        self.song.record_mode = toggled

    def _update_button_states(self):
        self._update_play_button_color()
        self._update_continue_playing_button_color()
        self._update_stop_button_color()

    def _update_play_button_color(self):
        raise NotImplementedError

    def _update_continue_playing_button_color(self):
        self.continue_playing_button.color = b'Transport.PlayOn' if self.song.is_playing else b'Transport.PlayOff'

    def _update_stop_button_color(self):
        self.stop_button.color = b'Transport.StopOff' if self.song.is_playing else b'Transport.StopOn'

    @listens(b'metronome')
    def __on_metronome_changed(self):
        self._update_tui_metronome_button()
        self._update_metronome_color_control()

    @listens(b'follow_song')
    def __on_follow_song_changed(self):
        self.follow_song_button.color = b'DefaultButton.On' if self.song.view.follow_song else b'DefaultButton.Off'

    @listens(b'clip_trigger_quantization')
    def __on_clip_trigger_quantization_changed(self):
        self._update_clip_trigger_quantization_control()
        self._update_clip_trigger_quantization_color_controls()

    @listens(b'signature_numerator')
    def __on_signature_numerator_changed(self):
        self._cached_num_beats_in_bar = num_beats_in_bar(self.song)

    @listens(b'signature_denominator')
    def __on_signature_denominator_changed(self):
        self._cached_num_beats_in_bar = num_beats_in_bar(self.song)

    def _update_clip_trigger_quantization_control(self):
        self.clip_trigger_quantization_control.value = int(
            self.song.clip_trigger_quantization)

    def _update_clip_trigger_quantization_color_controls(self):
        quantization = self.song.clip_trigger_quantization
        for index, control in enumerate(
                self.clip_trigger_quantization_color_controls):
            control.color = b'DefaultButton.On' if RADIO_BUTTON_GROUP_QUANTIZATION_VALUES[
                index] == quantization else b'DefaultButton.Off'

    def _update_tui_metronome_button(self):
        self.tui_metronome_button.is_toggled = self.song.metronome

    def _update_metronome_color_control(self):
        self.metronome_color_control.color = b'Transport.MetronomeOn' if self.song.metronome else b'Transport.MetronomeOff'

    def _update_tui_arrangement_record_button(self):
        self.tui_arrangement_record_button.is_toggled = self.song.record_mode

    @listens(b'loop_start')
    def __on_loop_start_changed(self):
        loop_start_time = self.song.get_beats_loop_start()
        self.loop_start_display[0] = format_beat_time(loop_start_time)

    @listens(b'loop_length')
    def __on_loop_length_changed(self):
        loop_length_time = self.song.get_beats_loop_length()
        self.loop_length_display[0] = format_beat_time(loop_length_time)

    @listens(b'current_song_time')
    def __on_arrangement_position_changed(self):
        song_time = self.song.get_current_beats_song_time()
        self.arrangement_position_display[0] = format_beat_time(song_time)

    @listens(b'record_mode')
    def __on_record_mode_changed(self):
        self._update_tui_arrangement_record_button()
class PrintToClipComponent(Component):
    print_to_clip_control = InputControl()
    print_to_clip_enabler = SendValueControl()

    def __init__(self, *a, **k):
        (super(PrintToClipComponent, self).__init__)(*a, **k)
        self._clip_data = {}
        self._last_packet_id = -1
        self._reset_last_packet_id_task = self._tasks.add(task.sequence(task.wait(RESET_PACKET_ID_TASK_DELAY), task.run(self._reset_last_packet_id)))
        self._reset_last_packet_id_task.kill()
        self._PrintToClipComponent__on_selected_track_changed.subject = self.song.view
        self._PrintToClipComponent__on_selected_track_changed()

    @print_to_clip_control.value
    def print_to_clip_control(self, data_bytes, _):
        self._reset_last_packet_id_task.restart()
        packet_id = sum_multi_byte_value((data_bytes[PACKET_ID_SLICE]), bits_per_byte=4)
        if packet_id != 0:
            if packet_id - 1 != self._last_packet_id:
                self.show_message(PACKET_ERROR_MESSAGE)
                return
        num_bytes = len(data_bytes)
        transfer_type = data_bytes[MESSAGE_TYPE_INDEX]
        if transfer_type == MessageType.begin:
            self._clip_data = {'notes': []}
        elif transfer_type == MessageType.data and num_bytes >= MIN_DATA_PACKET_LENGTH:
            self._handle_data_packet(data_bytes)
        elif transfer_type == MessageType.end:
            self._print_data_to_clip()
        self._last_packet_id = packet_id

    def _handle_data_packet(self, data_bytes):
        payload = data_bytes[PAYLOAD_START_INDEX:]
        if len(payload) == BYTES_PER_GROUP_OFFSET:
            self._clip_data['length'] = to_absolute_beat_time(payload)
        else:
            group_offset = to_absolute_beat_time(payload[:BYTES_PER_GROUP_OFFSET])
            payload = payload[BYTES_PER_GROUP_OFFSET:]
            payload_length = len(payload)
            if payload_length % BYTES_PER_NOTE == 0:
                self._clip_data['notes'].extend([create_note(payload[i:i + BYTES_PER_NOTE], group_offset) for i in range(0, payload_length, BYTES_PER_NOTE)])

    def _reset_last_packet_id(self):
        self._last_packet_id = -1

    def _print_data_to_clip(self):
        if 'length' in self._clip_data:
            clip = self._create_clip(self._clip_data['length'])
            if liveobj_valid(clip):
                self._wrap_trailing_notes()
                note_data = sorted((self._clip_data['notes']), key=(itemgetter(1)))
                notes = tuple((Live.Clip.MidiNoteSpecification(pitch=(note[Note.pitch]), start_time=(note[Note.start]), duration=(note[Note.length]), velocity=(note[Note.velocity]), mute=(note[Note.mute])) for note in note_data))
                clip.add_new_notes(notes)

    def _create_clip(self, length):
        song = self.song
        view = song.view
        track = view.selected_track
        try:
            scene_index = list(song.scenes).index(view.selected_scene)
            scene_count = len(song.scenes)
            while track.clip_slots[scene_index].has_clip:
                scene_index += 1
                if scene_index == scene_count:
                    song.create_scene(scene_count)

            slot = track.clip_slots[scene_index]
            slot.create_clip(length)
            return slot.clip
        except Live.Base.LimitationError:
            self.show_message(LIMITATION_ERROR_MESSAGE)
            return

    def _wrap_trailing_notes(self):
        for note in self._clip_data['notes'][:]:
            note_end_position = note[Note.start] + note[Note.length]
            if note_end_position > self._clip_data['length']:
                wrapped_note_length = note_end_position - self._clip_data['length'] + WRAPPED_NOTE_OFFSET
                self._clip_data['notes'].append((
                 note[Note.pitch],
                 -WRAPPED_NOTE_OFFSET,
                 wrapped_note_length,
                 note[Note.velocity],
                 note[Note.mute]))

    @listens('selected_track')
    def __on_selected_track_changed(self):
        can_print = self.song.view.selected_track.has_midi_input
        self.print_to_clip_control.enabled = can_print
        self.print_to_clip_enabler.value = int(can_print)
class PrintToClipComponent(Component):
    u"""
    Component that handles the print to clip functionality (whereby we receive SysEx
    messages that represent note data to write to a MIDI clip in Live) and workflow for
    Novation products.
    
    The print to clip SysEx API is as follows.
    
    CONTENT TRANSFER SYSEX MESSAGE FORMAT
    --------------------------------------------------------------------------------------
    | BYTE(S)             |  DESCRIPTION
    --------------------------------------------------------------------------------------
    | Message Type (0)    |  0x01 - Begin transfer
    |                     |
    |                     |  Indicates the start of a transfer.
    |                     |
    |                     |  0x02 - Data packet
    |                     |
    |                     |  Packet of data as part of the transfer.
    |                     |
    |                     |  0x03 - End transfer
    |                     |
    |                     |  Indicates the end of a transfer.
    |                     |
    --------------------------------------------------------------------------------------
    | Packet ID (1 - 8)   |  8 bytes indicating the packet ID as an integer. This will
    |                     |  be 0 for the Begin Transfer Message Type and increase by 1
    |                     |  for each subsequent packet.
    |                     |
    |                     |  This is used to validate that no packets were lost in the
    |                     |  transfer.
    |                     |
    --------------------------------------------------------------------------------------
    | Content Type (9)    |  Not used.
    |                     |
     --------------------------------------------------------------------------------------
    | Content Index (10)  |  Not used.
    |                     |
    --------------------------------------------------------------------------------------
    | Payload (11 - ?)    |  Multiple bytes that depend on the Message Type. The only
    |                     |  payload we care about is that of data packets. The contents
    |                     |  of those is described in the next table.
    |                     |
    --------------------------------------------------------------------------------------
    
    NOTES:
    A content transfer complete with note data requires at least 4 SysEx messages:
    - Begin transfer
    - Data packet containing note data
    - Data packet containing the end time of the clip to create
    - End transfer
    
    A content transfer without note data requires 3 messages and will create an empty clip:
    - Begin transfer
    - Data packet containing the end time of the clip to create
    - End transfer
    
    
    DATA PACKET FORMAT FOR PRINT TO CLIP
    --------------------------------------------------------------------------------------
    | BYTE(S)               |  DESCRIPTION
    --------------------------------------------------------------------------------------
    | Group Offset (0 - 2)  |  Specifies either the starting offset for all of the notes
    |                       |  that follow or the absolute length of the clip to create.
    |                       |
    |                       |  In the case of the latter, this will be the only bytes
    |                       |  we deal with.
    |                       |
    --------------------------------------------------------------------------------------
    | Start Time (3 - 4)    |  The start time of the note relative to the Group Offset.
    |                       |
    --------------------------------------------------------------------------------------
    | Length (5 - 6)        |  The length of the note.
    |                       |
    --------------------------------------------------------------------------------------
    | Pitch (7)             |  The pitch of the note.
    |                       |
    --------------------------------------------------------------------------------------
    | Velocity (8)          |  The velocity of the note.
    |                       |
    --------------------------------------------------------------------------------------
    
    NOTES:
    - Timing information is in ms and based on a tempo of 120 BPM.
    - Aside from the Group Offset, all other bytes can be repeated any number of times.
    """
    print_to_clip_control = InputControl()
    print_to_clip_enabler = SendValueControl()

    def __init__(self, *a, **k):
        super(PrintToClipComponent, self).__init__(*a, **k)
        self._clip_data = {}
        self._last_packet_id = -1
        self._reset_last_packet_id_task = self._tasks.add(
            task.sequence(task.wait(RESET_PACKET_ID_TASK_DELAY),
                          task.run(self._reset_last_packet_id)))
        self._reset_last_packet_id_task.kill()
        self.__on_selected_track_changed.subject = self.song.view
        self.__on_selected_track_changed()

    @print_to_clip_control.value
    def print_to_clip_control(self, data_bytes, _):
        self._reset_last_packet_id_task.restart()
        packet_id = sum_multi_byte_value(data_bytes[PACKET_ID_SLICE],
                                         bits_per_byte=4)
        if packet_id != 0 and packet_id - 1 != self._last_packet_id:
            self.show_message(PACKET_ERROR_MESSAGE)
            return
        num_bytes = len(data_bytes)
        transfer_type = data_bytes[MESSAGE_TYPE_INDEX]
        if transfer_type == MessageType.begin:
            self._clip_data = {u'notes': []}
        elif transfer_type == MessageType.data and num_bytes >= MIN_DATA_PACKET_LENGTH:
            self._handle_data_packet(data_bytes)
        elif transfer_type == MessageType.end:
            self._print_data_to_clip()
        self._last_packet_id = packet_id

    def _handle_data_packet(self, data_bytes):
        payload = data_bytes[PAYLOAD_START_INDEX:]
        if len(payload) == BYTES_PER_GROUP_OFFSET:
            self._clip_data[u'length'] = to_absolute_beat_time(payload)
        else:
            group_offset = to_absolute_beat_time(
                payload[:BYTES_PER_GROUP_OFFSET])
            payload = payload[BYTES_PER_GROUP_OFFSET:]
            payload_length = len(payload)
            if payload_length % BYTES_PER_NOTE == 0:
                self._clip_data[u'notes'].extend([
                    create_note(payload[i:i + BYTES_PER_NOTE], group_offset)
                    for i in range(0, payload_length, BYTES_PER_NOTE)
                ])

    def _reset_last_packet_id(self):
        self._last_packet_id = -1

    def _print_data_to_clip(self):
        if u'length' in self._clip_data:
            clip = self._create_clip(self._clip_data[u'length'])
            if liveobj_valid(clip):
                self._wrap_trailing_notes()
                note_data = sorted(self._clip_data[u'notes'],
                                   key=itemgetter(1))
                notes = tuple((Live.Clip.MidiNoteSpecification(
                    pitch=note[Note.pitch],
                    start_time=note[Note.start],
                    duration=note[Note.length],
                    velocity=note[Note.velocity],
                    mute=note[Note.mute]) for note in note_data))
                clip.add_new_notes(notes)

    def _create_clip(self, length):
        song = self.song
        view = song.view
        track = view.selected_track
        try:
            scene_index = list(song.scenes).index(view.selected_scene)
            scene_count = len(song.scenes)
            while track.clip_slots[scene_index].has_clip:
                scene_index += 1
                if scene_index == scene_count:
                    song.create_scene(scene_count)

            slot = track.clip_slots[scene_index]
            slot.create_clip(length)
            return slot.clip
        except Live.Base.LimitationError:
            self.show_message(LIMITATION_ERROR_MESSAGE)
            return None

    def _wrap_trailing_notes(self):
        for note in self._clip_data[u'notes'][:]:
            note_end_position = note[Note.start] + note[Note.length]
            if note_end_position > self._clip_data[u'length']:
                wrapped_note_length = note_end_position - self._clip_data[
                    u'length'] + WRAPPED_NOTE_OFFSET
                self._clip_data[u'notes'].append(
                    (note[Note.pitch], -WRAPPED_NOTE_OFFSET,
                     wrapped_note_length, note[Note.velocity],
                     note[Note.mute]))

    @listens(u'selected_track')
    def __on_selected_track_changed(self):
        can_print = self.song.view.selected_track.has_midi_input
        self.print_to_clip_control.enabled = can_print
        self.print_to_clip_enabler.value = int(can_print)