1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# A piano roll viewer/editor
5# Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
6# Copyright (C) 2014-2015 Perry Nguyen
7#
8# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License as
10# published by the Free Software Foundation; either version 2 of
11# the License, or any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# For a full copy of the GNU General Public License see the doc/GPL.txt file.
19
20# ------------------------------------------------------------------------------------------------------------
21# Imports (Global)
22
23from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
24from PyQt5.QtGui import QColor, QCursor, QFont, QPen, QPainter, QTransform
25from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
26from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
27from PyQt5.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
28
29# ------------------------------------------------------------------------------------------------------------
30# Imports (Custom)
31
32from carla_shared import *
33
34# ------------------------------------------------------------------------------------------------------------
35# MIDI definitions, copied from CarlaMIDI.h
36
37MAX_MIDI_CHANNELS = 16
38MAX_MIDI_NOTE     = 128
39MAX_MIDI_VALUE    = 128
40MAX_MIDI_CONTROL  = 120 # 0x77
41
42MIDI_STATUS_BIT  = 0xF0
43MIDI_CHANNEL_BIT = 0x0F
44
45# MIDI Messages List
46MIDI_STATUS_NOTE_OFF              = 0x80 # note (0-127), velocity (0-127)
47MIDI_STATUS_NOTE_ON               = 0x90 # note (0-127), velocity (0-127)
48MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
49MIDI_STATUS_CONTROL_CHANGE        = 0xB0 # see 'Control Change Messages List'
50MIDI_STATUS_PROGRAM_CHANGE        = 0xC0 # program (0-127), none
51MIDI_STATUS_CHANNEL_PRESSURE      = 0xD0 # pressure (0-127), none
52MIDI_STATUS_PITCH_WHEEL_CONTROL   = 0xE0 # LSB (0-127), MSB (0-127)
53
54# MIDI Message type
55def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status <  MIDI_STATUS_BIT
56def MIDI_IS_SYSTEM_MESSAGE(status):  return status >= MIDI_STATUS_BIT      and status <= 0xFF
57def MIDI_IS_OSC_MESSAGE(status):     return status == '/'                   or status == '#'
58
59# MIDI Channel message type
60def MIDI_IS_STATUS_NOTE_OFF(status):              return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
61def MIDI_IS_STATUS_NOTE_ON(status):               return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
62def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
63def MIDI_IS_STATUS_CONTROL_CHANGE(status):        return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
64def MIDI_IS_STATUS_PROGRAM_CHANGE(status):        return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
65def MIDI_IS_STATUS_CHANNEL_PRESSURE(status):      return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
66def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status):   return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL
67
68# MIDI Utils
69def MIDI_GET_STATUS_FROM_DATA(data):  return data[0] & MIDI_STATUS_BIT  if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
70def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0
71
72# ---------------------------------------------------------------------------------------------------------------------
73# Graphics Items
74
75class NoteExpander(QGraphicsRectItem):
76    def __init__(self, length, height, parent):
77        QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
78        self.parent = parent
79        self.orig_brush = QColor(0, 0, 0, 0)
80        self.hover_brush = QColor(200, 200, 200)
81        self.stretch = False
82
83        self.setAcceptHoverEvents(True)
84        self.setFlag(QGraphicsItem.ItemIsSelectable)
85        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
86        self.setPen(QPen(QColor(0,0,0,0)))
87
88    def paint(self, painter, option, widget=None):
89        paint_option = option
90        paint_option.state &= ~QStyle.State_Selected
91        QGraphicsRectItem.paint(self, painter, paint_option, widget)
92
93    def mousePressEvent(self, event):
94        QGraphicsRectItem.mousePressEvent(self, event)
95        self.stretch = True
96
97    def mouseReleaseEvent(self, event):
98        QGraphicsRectItem.mouseReleaseEvent(self, event)
99        self.stretch = False
100
101    def hoverEnterEvent(self, event):
102        QGraphicsRectItem.hoverEnterEvent(self, event)
103        self.setCursor(QCursor(Qt.SizeHorCursor))
104        self.setBrush(self.hover_brush)
105
106    def hoverLeaveEvent(self, event):
107        QGraphicsRectItem.hoverLeaveEvent(self, event)
108        self.unsetCursor()
109        self.setBrush(self.orig_brush)
110
111# ---------------------------------------------------------------------------------------------------------------------
112
113class NoteItem(QGraphicsRectItem):
114    '''a note on the pianoroll sequencer'''
115    def __init__(self, height, length, note_info):
116        QGraphicsRectItem.__init__(self, 0, 0, length, height)
117
118        self.orig_brush = QColor(note_info[3], 0, 0)
119        self.hover_brush = QColor(note_info[3] + 98, 200, 100)
120        self.select_brush = QColor(note_info[3] + 98, 100, 100)
121
122        self.note = note_info
123        self.length = length
124        self.piano = self.scene
125
126        self.pressed = False
127        self.hovering = False
128        self.moving_diff = (0,0)
129        self.expand_diff = 0
130
131        self.setAcceptHoverEvents(True)
132        self.setFlag(QGraphicsItem.ItemIsMovable)
133        self.setFlag(QGraphicsItem.ItemIsSelectable)
134        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
135        self.setPen(QPen(QColor(0,0,0,0)))
136        self.setBrush(self.orig_brush)
137
138        l = 5
139        self.front = NoteExpander(l, height, self)
140        self.back = NoteExpander(l, height, self)
141        self.back.setPos(length - l, 0)
142
143    def paint(self, painter, option, widget=None):
144        paint_option = option
145        paint_option.state &= ~QStyle.State_Selected
146        if self.isSelected():
147            self.setBrush(self.select_brush)
148        elif self.hovering:
149            self.setBrush(self.hover_brush)
150        else:
151            self.setBrush(self.orig_brush)
152        QGraphicsRectItem.paint(self, painter, paint_option, widget)
153
154    def hoverEnterEvent(self, event):
155        QGraphicsRectItem.hoverEnterEvent(self, event)
156        self.hovering = True
157        self.update()
158        self.setCursor(QCursor(Qt.OpenHandCursor))
159
160    def hoverLeaveEvent(self, event):
161        QGraphicsRectItem.hoverLeaveEvent(self, event)
162        self.hovering = False
163        self.unsetCursor()
164        self.update()
165
166    def mousePressEvent(self, event):
167        QGraphicsRectItem.mousePressEvent(self, event)
168        self.pressed = True
169        self.moving_diff = (0,0)
170        self.expand_diff = 0
171        self.setCursor(QCursor(Qt.ClosedHandCursor))
172        self.setSelected(True)
173
174    def mouseMoveEvent(self, event):
175        event.ignore()
176
177    def mouseReleaseEvent(self, event):
178        QGraphicsRectItem.mouseReleaseEvent(self, event)
179        self.pressed = False
180        self.moving_diff = (0,0)
181        self.expand_diff = 0
182        self.setCursor(QCursor(Qt.OpenHandCursor))
183
184    def moveEvent(self, event):
185        offset = event.scenePos() - event.lastScenePos()
186
187        if self.back.stretch:
188            self.expand(self.back, offset)
189            self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
190            return
191
192        if self.front.stretch:
193            self.expand(self.front, offset)
194            self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
195            return
196
197        piano = self.piano()
198
199        pos = self.scenePos() + offset + QPointF(self.moving_diff[0],self.moving_diff[1])
200        pos = piano.enforce_bounds(pos)
201        pos_x = pos.x()
202        pos_y = pos.y()
203        width = self.rect().width()
204        if pos_x + width > piano.grid_width + piano.piano_width:
205            pos_x = piano.grid_width + piano.piano_width - width
206        pos_sx, pos_sy = piano.snap(pos_x, pos_y)
207
208        if pos_sx + width > piano.grid_width + piano.piano_width:
209            self.moving_diff = (0,0)
210            self.expand_diff = 0
211            return
212
213        self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
214        self.setPos(pos_sx, pos_sy)
215
216        self.updateNoteInfo(pos_sx, pos_sy)
217
218    def expand(self, rectItem, offset):
219        rect = self.rect()
220        piano = self.piano()
221        width = rect.right() + self.expand_diff
222
223        if rectItem == self.back:
224            width += offset.x()
225            max_x = piano.grid_width + piano.piano_width
226            if width + self.scenePos().x() >= max_x:
227                width = max_x - self.scenePos().x() - 1
228            elif piano.snap_value and width < piano.snap_value:
229                width = piano.snap_value
230            elif width < 10:
231                width = 10
232            new_w = piano.snap(width) - 2.75
233            if new_w + self.scenePos().x() >= max_x:
234                self.moving_diff = (0,0)
235                self.expand_diff = 0
236                return
237
238        else:
239            width -= offset.x()
240            new_w = piano.snap(width+2.75) - 2.75
241            if new_w <= 0:
242                new_w = piano.snap_value
243                self.moving_diff = (0,0)
244                self.expand_diff = 0
245                return
246            diff = rect.right() - new_w
247            if diff: # >= piano.snap_value:
248                new_x = self.scenePos().x() + diff
249                if new_x < piano.piano_width:
250                    new_x = piano.piano_width
251                    self.moving_diff = (0,0)
252                    self.expand_diff = 0
253                    return
254                print(new_x, new_w, diff)
255                self.setX(new_x)
256
257        self.expand_diff = width - new_w
258        self.back.setPos(new_w - 5, 0)
259        rect.setRight(new_w)
260        self.setRect(rect)
261
262    def updateNoteInfo(self, pos_x, pos_y):
263        note_info = (self.piano().get_note_num_from_y(pos_y),
264                     self.piano().get_note_start_from_x(pos_x),
265                     self.piano().get_note_length_from_x(self.rect().width()),
266                     self.note[3])
267        if self.note != note_info:
268            self.piano().move_note(self.note, note_info)
269            self.note = note_info
270
271    def updateVelocity(self, event):
272        offset = event.scenePos().x() - event.lastScenePos().x()
273        offset = int(offset/5)
274
275        note_info = self.note[:]
276        note_info[3] += offset
277        if note_info[3] > 127:
278            note_info[3] = 127
279        elif note_info[3] < 0:
280            note_info[3] = 0
281        if self.note != note_info:
282            self.orig_brush = QColor(note_info[3], 0, 0)
283            self.hover_brush = QColor(note_info[3] + 98, 200, 100)
284            self.select_brush = QColor(note_info[3] + 98, 100, 100)
285            self.update()
286            self.piano().move_note(self.note, note_info)
287            self.note = note_info
288
289# ---------------------------------------------------------------------------------------------------------------------
290
291class PianoKeyItem(QGraphicsRectItem):
292    def __init__(self, width, height, note, parent):
293        QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
294
295        self.width = width
296        self.height = height
297        self.note = note
298        self.piano = self.scene
299        self.hovered = False
300        self.pressed = False
301
302        self.click_brush = QColor(255, 100, 100)
303        self.hover_brush = QColor(200, 0, 0)
304        self.orig_brush = None
305
306        self.setAcceptHoverEvents(True)
307        self.setFlag(QGraphicsItem.ItemIsSelectable)
308        self.setPen(QPen(QColor(0,0,0,80)))
309
310    def paint(self, painter, option, widget=None):
311        paint_option = option
312        paint_option.state &= ~QStyle.State_Selected
313        QGraphicsRectItem.paint(self, painter, paint_option, widget)
314
315    def hoverEnterEvent(self, event):
316        QGraphicsRectItem.hoverEnterEvent(self, event)
317        self.hovered = True
318        self.orig_brush = self.brush()
319        self.setBrush(self.hover_brush)
320
321    def hoverLeaveEvent(self, event):
322        QGraphicsRectItem.hoverLeaveEvent(self, event)
323        self.hovered = False
324        self.setBrush(self.click_brush if self.pressed else self.orig_brush)
325
326    def mousePressEvent(self, event):
327        QGraphicsRectItem.mousePressEvent(self, event)
328        self.pressed = True
329        self.setBrush(self.click_brush)
330        self.piano().noteclicked.emit(self.note, True)
331
332    def mouseReleaseEvent(self, event):
333        QGraphicsRectItem.mouseReleaseEvent(self, event)
334        self.pressed = False
335        self.setBrush(self.hover_brush if self.hovered else self.orig_brush)
336        self.piano().noteclicked.emit(self.note, False)
337
338# ---------------------------------------------------------------------------------------------------------------------
339
340class PianoRoll(QGraphicsScene):
341    '''the piano roll'''
342
343    noteclicked = pyqtSignal(int,bool)
344    midievent = pyqtSignal(list)
345    measureupdate = pyqtSignal(int)
346    modeupdate = pyqtSignal(str)
347
348    default_ghost_vel = 100
349
350    def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
351        QGraphicsScene.__init__(self)
352        self.setBackgroundBrush(QColor(50, 50, 50))
353
354        self.notes = []
355        self.removed_notes = []
356        self.selected_notes = []
357        self.piano_keys = []
358
359        self.marquee_select = False
360        self.marquee_rect = None
361        self.marquee = None
362
363        self.ghost_note = None
364        self.ghost_rect = None
365        self.ghost_rect_orig_width = None
366        self.ghost_vel = self.default_ghost_vel
367
368        self.ignore_mouse_events = False
369        self.insert_mode = False
370        self.velocity_mode = False
371        self.place_ghost = False
372        self.last_mouse_pos = QPointF()
373
374        ## dimensions
375        self.padding = 2
376
377        ## piano dimensions
378        self.note_height = 10
379        self.start_octave = -2
380        self.end_octave = 8
381        self.notes_in_octave = 12
382        self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1
383        self.piano_height = self.note_height * self.total_notes
384        self.octave_height = self.notes_in_octave * self.note_height
385
386        self.piano_width = 34
387
388        ## height
389        self.header_height = 20
390        self.total_height = self.piano_height - self.note_height + self.header_height
391        #not sure why note_height is subtracted
392
393        ## width
394        self.full_note_width = 250 # i.e. a 4/4 note
395        self.snap_value = None
396        self.quantize_val = quantize_val
397
398        ### dummy vars that will be changed
399        self.time_sig = (0,0)
400        self.measure_width = 0
401        self.num_measures = 0
402        self.max_note_length = 0
403        self.grid_width = 0
404        self.value_width = 0
405        self.grid_div = 0
406        self.piano = None
407        self.header = None
408        self.play_head = None
409
410        self.setGridDiv()
411        self.default_length = 1. / self.grid_div
412
413
414    # -------------------------------------------------------------------------
415    # Callbacks
416
417    def movePlayHead(self, transportInfo):
418        ticksPerBeat = transportInfo['ticksPerBeat']
419        max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures
420        cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick']
421        frac = (cur_tick % max_ticks) / max_ticks
422        self.play_head.setPos(QPointF(frac * self.grid_width, 0))
423
424    def setTimeSig(self, time_sig):
425        self.time_sig = time_sig
426        self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
427        self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
428        self.grid_width = self.measure_width * self.num_measures
429        self.setGridDiv()
430
431    def setMeasures(self, measures):
432        #try:
433        self.num_measures = float(measures)
434        self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
435        self.grid_width = self.measure_width * self.num_measures
436        self.refreshScene()
437        #except:
438            #pass
439
440    def setDefaultLength(self, length):
441        v = list(map(float, length.split('/')))
442        if len(v) < 3:
443            self.default_length = v[0] if len(v) == 1 else v[0] / v[1]
444            pos = self.enforce_bounds(self.last_mouse_pos)
445            if self.insert_mode:
446                self.makeGhostNote(pos.x(), pos.y())
447
448    def setGridDiv(self, div=None):
449        if not div: div = self.quantize_val
450        try:
451            val = list(map(int, div.split('/')))
452            if len(val) < 3:
453                self.quantize_val = div
454                self.grid_div = val[0] if len(val)==1 else val[1]
455                self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
456                self.setQuantize(div)
457
458                self.refreshScene()
459        except ValueError:
460            pass
461
462    def setQuantize(self, value):
463        val = list(map(float, value.split('/')))
464        if len(val) == 1:
465            self.quantize(val[0])
466            self.quantize_val = value
467        elif len(val) == 2:
468            self.quantize(val[0] / val[1])
469            self.quantize_val = value
470
471    # -------------------------------------------------------------------------
472    # Event Callbacks
473
474    def keyPressEvent(self, event):
475        QGraphicsScene.keyPressEvent(self, event)
476
477        if event.key() == Qt.Key_Escape:
478            QApplication.instance().closeAllWindows()
479            return
480
481        if event.key() == Qt.Key_F:
482            if not self.insert_mode:
483                # turn off velocity mode
484                self.velocity_mode = False
485                # enable insert mode
486                self.insert_mode = True
487                self.place_ghost = False
488                self.makeGhostNote(self.last_mouse_pos.x(), self.last_mouse_pos.y())
489                self.modeupdate.emit('insert_mode')
490            else:
491                # turn off insert mode
492                self.insert_mode = False
493                self.place_ghost = False
494                if self.ghost_note is not None:
495                    self.removeItem(self.ghost_note)
496                    self.ghost_note = None
497                self.modeupdate.emit('')
498
499        elif event.key() == Qt.Key_D:
500            if not self.velocity_mode:
501                # turn off insert mode
502                self.insert_mode = False
503                self.place_ghost = False
504                if self.ghost_note is not None:
505                    self.removeItem(self.ghost_note)
506                    self.ghost_note = None
507                # enable velocity mode
508                self.velocity_mode = True
509                self.modeupdate.emit('velocity_mode')
510            else:
511                # turn off velocity mode
512                self.velocity_mode = False
513                self.modeupdate.emit('')
514
515        elif event.key() == Qt.Key_A:
516            for note in self.notes:
517                if not note.isSelected():
518                    has_unselected = True
519                    break
520            else:
521                has_unselected = False
522
523            # select all notes
524            if has_unselected:
525                for note in self.notes:
526                    note.setSelected(True)
527                self.selected_notes = self.notes[:]
528            # unselect all
529            else:
530                for note in self.notes:
531                    note.setSelected(False)
532                self.selected_notes = []
533
534        elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
535            # remove selected notes from our notes list
536            self.notes = [note for note in self.notes if note not in self.selected_notes]
537            # delete the selected notes
538            for note in self.selected_notes:
539                self.removeItem(note)
540                self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
541                del note
542            self.selected_notes = []
543
544    def mousePressEvent(self, event):
545        QGraphicsScene.mousePressEvent(self, event)
546
547        # mouse click on left-side piano area
548        if self.piano.contains(event.scenePos()):
549            self.ignore_mouse_events = True
550            return
551
552        clicked_notes = []
553
554        for note in self.notes:
555            if note.pressed or note.back.stretch or note.front.stretch:
556                clicked_notes.append(note)
557
558        # mouse click on existing notes
559        if clicked_notes:
560            keep_selection = all(note in self.selected_notes for note in clicked_notes)
561            if keep_selection:
562                for note in self.selected_notes:
563                    note.setSelected(True)
564                return
565
566            for note in self.selected_notes:
567                if note not in clicked_notes:
568                    note.setSelected(False)
569            for note in clicked_notes:
570                if note not in self.selected_notes:
571                    note.setSelected(True)
572
573            self.selected_notes = clicked_notes
574            return
575
576        # mouse click on empty area (no note selected)
577        for note in self.selected_notes:
578            note.setSelected(False)
579        self.selected_notes = []
580
581        if event.button() != Qt.LeftButton:
582            return
583
584        if self.insert_mode:
585            self.place_ghost = True
586        else:
587            self.marquee_select = True
588            self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
589            self.marquee = QGraphicsRectItem(self.marquee_rect)
590            self.marquee.setBrush(QColor(255, 255, 255, 100))
591            self.addItem(self.marquee)
592
593    def mouseMoveEvent(self, event):
594        QGraphicsScene.mouseMoveEvent(self, event)
595
596        self.last_mouse_pos = event.scenePos()
597
598        if self.ignore_mouse_events:
599            return
600
601        pos = self.enforce_bounds(self.last_mouse_pos)
602
603        if self.insert_mode:
604            if self.ghost_note is None:
605                self.makeGhostNote(pos.x(), pos.y())
606            max_x = self.grid_width + self.piano_width
607
608            # placing note, only width needs updating
609            if self.place_ghost:
610                pos_x = pos.x()
611                min_x = self.ghost_rect.x() + self.ghost_rect_orig_width
612                if pos_x < min_x:
613                    pos_x = min_x
614                new_x = self.snap(pos_x)
615                self.ghost_rect.setRight(new_x)
616                self.ghost_note.setRect(self.ghost_rect)
617                #self.adjust_note_vel(event)
618
619            # ghostnote following mouse around
620            else:
621                pos_x = pos.x()
622                if pos_x + self.ghost_rect.width() >= max_x:
623                    pos_x = max_x - self.ghost_rect.width()
624                elif pos_x > self.piano_width + self.ghost_rect.width()*3/4:
625                    pos_x -= self.ghost_rect.width()/2
626                new_x, new_y = self.snap(pos_x, pos.y())
627                self.ghost_rect.moveTo(new_x, new_y)
628                self.ghost_note.setRect(self.ghost_rect)
629            return
630
631        if self.marquee_select:
632            marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
633            if marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() < pos.y():
634                self.marquee_rect.setBottomRight(pos)
635            elif marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() > pos.y():
636                self.marquee_rect.setTopRight(pos)
637            elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() < pos.y():
638                self.marquee_rect.setBottomLeft(pos)
639            elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() > pos.y():
640                self.marquee_rect.setTopLeft(pos)
641            self.marquee.setRect(self.marquee_rect)
642
643            for note in self.selected_notes:
644                note.setSelected(False)
645            self.selected_notes = []
646
647            for item in self.collidingItems(self.marquee):
648                if item in self.notes:
649                    item.setSelected(True)
650                    self.selected_notes.append(item)
651            return
652
653        if event.buttons() != Qt.LeftButton:
654            return
655
656        if self.velocity_mode:
657            for note in self.selected_notes:
658                note.updateVelocity(event)
659            return
660
661        x = y = False
662        for note in self.selected_notes:
663            if note.back.stretch:
664                x = True
665                break
666        for note in self.selected_notes:
667            if note.front.stretch:
668                y = True
669                break
670        for note in self.selected_notes:
671            note.back.stretch = x
672            note.front.stretch = y
673            note.moveEvent(event)
674
675    def mouseReleaseEvent(self, event):
676        QGraphicsScene.mouseReleaseEvent(self, event)
677
678        if self.ignore_mouse_events:
679            self.ignore_mouse_events = False
680            return
681
682        if self.marquee_select:
683            self.marquee_select = False
684            self.removeItem(self.marquee)
685            self.marquee = None
686
687        if self.insert_mode and self.place_ghost:
688            self.place_ghost = False
689            note_start = self.get_note_start_from_x(self.ghost_rect.x())
690            note_num = self.get_note_num_from_y(self.ghost_rect.y())
691            note_length = self.get_note_length_from_x(self.ghost_rect.width())
692            note = self.drawNote(note_num, note_start, note_length, self.ghost_vel)
693            note.setSelected(True)
694            self.selected_notes.append(note)
695            self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
696            pos = self.enforce_bounds(self.last_mouse_pos)
697            pos_x = pos.x()
698            if pos_x > self.piano_width + self.ghost_rect.width()*3/4:
699                pos_x -= self.ghost_rect.width()/2
700            self.makeGhostNote(pos_x, pos.y())
701
702        for note in self.selected_notes:
703            note.back.stretch = False
704            note.front.stretch = False
705
706    # -------------------------------------------------------------------------
707    # Internal Functions
708
709    def drawHeader(self):
710        self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
711        #self.header.setZValue(1.0)
712        self.header.setPos(self.piano_width, 0)
713        self.addItem(self.header)
714
715    def drawPiano(self):
716        piano_keys_width = self.piano_width - self.padding
717        labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
718        black_notes = (2,4,6,9,11)
719        piano_label = QFont()
720        piano_label.setPointSize(6)
721        self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
722        self.piano.setPos(0, self.header_height)
723        self.addItem(self.piano)
724
725        key = PianoKeyItem(piano_keys_width, self.note_height, 78, self.piano)
726        label = QGraphicsSimpleTextItem('C9', key)
727        label.setPos(18, 1)
728        label.setFont(piano_label)
729        key.setBrush(QColor(255, 255, 255))
730        for i in range(self.end_octave - self.start_octave, 0, -1):
731            for j in range(self.notes_in_octave, 0, -1):
732                note = (self.end_octave - i + 3) * 12 - j
733                if j in black_notes:
734                    key = PianoKeyItem(piano_keys_width/1.4, self.note_height, note, self.piano)
735                    key.setBrush(QColor(0, 0, 0))
736                    key.setZValue(1.0)
737                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
738                elif (j - 1) and (j + 1) in black_notes:
739                    key = PianoKeyItem(piano_keys_width, self.note_height * 2, note, self.piano)
740                    key.setBrush(QColor(255, 255, 255))
741                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
742                elif (j - 1) in black_notes:
743                    key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
744                    key.setBrush(QColor(255, 255, 255))
745                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
746                elif (j + 1) in black_notes:
747                    key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
748                    key.setBrush(QColor(255, 255, 255))
749                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
750                if j == 12:
751                    label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i + 1), key)
752                    label.setPos(18, 6)
753                    label.setFont(piano_label)
754                self.piano_keys.append(key)
755
756    def drawGrid(self):
757        black_notes = [2,4,6,9,11]
758        scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
759        scale_bar.setPos(self.piano_width, 0)
760        scale_bar.setBrush(QColor(100,100,100))
761        clearpen = QPen(QColor(0,0,0,0))
762        for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
763            for j in range(self.notes_in_octave, 0, -1):
764                scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
765                scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
766                scale_bar.setPen(clearpen)
767                if j not in black_notes:
768                    scale_bar.setBrush(QColor(120,120,120))
769                else:
770                    scale_bar.setBrush(QColor(100,100,100))
771
772        measure_pen = QPen(QColor(0, 0, 0, 120), 3)
773        half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
774        line_pen = QPen(QColor(0, 0, 0, 40))
775        for i in range(0, int(self.num_measures) + 1):
776            measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
777            measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
778            measure.setPen(measure_pen)
779            if i < self.num_measures:
780                number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
781                number.setPos(self.measure_width * i + 5, 2)
782                number.setBrush(Qt.white)
783                for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
784                    line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
785                    line.setZValue(1.0)
786                    line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
787                    if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
788                        line.setPen(half_measure_pen)
789                    else:
790                        line.setPen(line_pen)
791
792    def drawPlayHead(self):
793        self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
794        self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
795        self.play_head.setZValue(1.)
796        self.addItem(self.play_head)
797
798    def refreshScene(self):
799        list(map(self.removeItem, self.notes))
800        self.selected_notes = []
801        self.piano_keys = []
802        self.place_ghost = False
803        if self.ghost_note is not None:
804            self.removeItem(self.ghost_note)
805            self.ghost_note = None
806        self.clear()
807        self.drawPiano()
808        self.drawHeader()
809        self.drawGrid()
810        self.drawPlayHead()
811
812        for note in self.notes[:]:
813            if note.note[1] >= (self.num_measures * self.time_sig[0]):
814                self.notes.remove(note)
815                self.removed_notes.append(note)
816                #self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
817            elif note.note[2] > self.max_note_length:
818                new_note = note.note[:]
819                new_note[2] = self.max_note_length
820                self.notes.remove(note)
821                self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
822                self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
823                self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
824
825        for note in self.removed_notes[:]:
826            if note.note[1] < (self.num_measures * self.time_sig[0]):
827                self.removed_notes.remove(note)
828                self.notes.append(note)
829
830        list(map(self.addItem, self.notes))
831        if self.views():
832            self.views()[0].setSceneRect(self.itemsBoundingRect())
833
834    def clearNotes(self):
835        self.clear()
836        self.notes = []
837        self.removed_notes = []
838        self.selected_notes = []
839        self.drawPiano()
840        self.drawHeader()
841        self.drawGrid()
842
843    def makeGhostNote(self, pos_x, pos_y):
844        """creates the ghostnote that is placed on the scene before the real one is."""
845        if self.ghost_note is not None:
846            self.removeItem(self.ghost_note)
847        length = self.full_note_width * self.default_length
848        pos_x, pos_y = self.snap(pos_x, pos_y)
849        self.ghost_vel = self.default_ghost_vel
850        self.ghost_rect = QRectF(pos_x, pos_y, length, self.note_height)
851        self.ghost_rect_orig_width = self.ghost_rect.width()
852        self.ghost_note = QGraphicsRectItem(self.ghost_rect)
853        self.ghost_note.setBrush(QColor(230, 221, 45, 100))
854        self.addItem(self.ghost_note)
855
856    def drawNote(self, note_num, note_start, note_length, note_velocity, add=True):
857        """
858        note_num: midi number, 0 - 127
859        note_start: 0 - (num_measures * time_sig[0]) so this is in beats
860        note_length: 0 - (num_measures  * time_sig[0]/time_sig[1]) this is in measures
861        note_velocity: 0 - 127
862        """
863
864        info = [note_num, note_start, note_length, note_velocity]
865
866        if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
867            #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
868            while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
869                self.setMeasures(self.num_measures+1)
870            self.measureupdate.emit(self.num_measures)
871            self.refreshScene()
872
873        x_start = self.get_note_x_start(note_start)
874        if note_length > self.max_note_length:
875            note_length = self.max_note_length + 0.25
876        x_length = self.get_note_x_length(note_length)
877        y_pos = self.get_note_y_pos(note_num)
878
879        note = NoteItem(self.note_height, x_length, info)
880        note.setPos(x_start, y_pos)
881
882        self.notes.append(note)
883
884        if add:
885            self.addItem(note)
886
887        return note
888
889    # -------------------------------------------------------------------------
890    # Helper Functions
891
892    def frange(self, x, y, t):
893        while x < y:
894            yield x
895            x += t
896
897    def quantize(self, value):
898        self.snap_value = float(self.full_note_width) * value if value else None
899
900    def snap(self, pos_x, pos_y = None):
901        if self.snap_value:
902            pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) * self.snap_value + self.piano_width
903        if pos_y is not None:
904            pos_y = int((pos_y - self.header_height) / self.note_height) * self.note_height + self.header_height
905        return (pos_x, pos_y) if pos_y is not None else pos_x
906
907    def adjust_note_vel(self, event):
908        m_pos = event.scenePos()
909        #bind velocity to vertical mouse movement
910        self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
911        if self.ghost_vel < 0:
912            self.ghost_vel = 0
913        elif self.ghost_vel > 127:
914            self.ghost_vel = 127
915
916        m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
917        if m_pos.x() < m_width:
918            m_pos.setX(m_width)
919        m_new_x = self.snap(m_pos.x())
920        self.ghost_rect.setRight(m_new_x)
921        self.ghost_note.setRect(self.ghost_rect)
922
923    def enforce_bounds(self, pos):
924        pos = QPointF(pos)
925        if pos.x() < self.piano_width:
926            pos.setX(self.piano_width)
927        elif pos.x() >= self.grid_width + self.piano_width:
928            pos.setX(self.grid_width + self.piano_width - 1)
929        if pos.y() < self.header_height + self.padding:
930            pos.setY(self.header_height + self.padding)
931        return pos
932
933    def get_note_start_from_x(self, note_x):
934        return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
935
936    def get_note_x_start(self, note_start):
937        return self.piano_width + (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
938
939    def get_note_x_length(self, note_length):
940        return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
941
942    def get_note_length_from_x(self, note_x):
943        return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width * note_x
944
945    def get_note_y_pos(self, note_num):
946        return self.header_height + self.note_height * (self.total_notes - note_num - 1)
947
948    def get_note_num_from_y(self, note_y_pos):
949        return -(int((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
950
951    def move_note(self, old_note, new_note):
952        self.midievent.emit(["midievent-remove", old_note[0], old_note[1], old_note[2], old_note[3]])
953        self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
954
955# ------------------------------------------------------------------------------------------------------------
956
957class PianoRollView(QGraphicsView):
958    def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
959        QGraphicsView.__init__(self, parent)
960        self.piano = PianoRoll(time_sig, num_measures, quantize_val)
961        self.setScene(self.piano)
962        #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
963
964        x = 0   * self.sceneRect().width() + self.sceneRect().left()
965        y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
966        self.centerOn(x, y)
967
968        self.setAlignment(Qt.AlignLeft)
969        self.o_transform = self.transform()
970        self.zoom_x = 1
971        self.zoom_y = 1
972
973    def setZoomX(self, scale_x):
974        self.setTransform(self.o_transform)
975        self.zoom_x = 1 + scale_x / float(99) * 2
976        self.scale(self.zoom_x, self.zoom_y)
977
978    def setZoomY(self, scale_y):
979        self.setTransform(self.o_transform)
980        self.zoom_y = 1 + scale_y / float(99)
981        self.scale(self.zoom_x, self.zoom_y)
982
983# ------------------------------------------------------------------------------------------------------------
984
985class ModeIndicator(QWidget):
986    def __init__(self, parent):
987        QWidget.__init__(self, parent)
988        #self.setGeometry(0, 0, 30, 20)
989        self.mode = None
990        self.setFixedSize(30,20)
991
992    def paintEvent(self, event):
993        event.accept()
994
995        painter = QPainter(self)
996        painter.setPen(QPen(QColor(0, 0, 0, 0)))
997
998        if self.mode == 'velocity_mode':
999            painter.setBrush(QColor(127, 0, 0))
1000        elif self.mode == 'insert_mode':
1001            painter.setBrush(QColor(0, 100, 127))
1002        else:
1003            painter.setBrush(QColor(0, 0, 0, 0))
1004
1005        painter.drawRect(0, 0, 30, 20)
1006
1007    def changeMode(self, new_mode):
1008        self.mode = new_mode
1009        self.update()
1010
1011# ------------------------------------------------------------------------------------------------------------
1012