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