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 # ------------------------------------------------------------------------------------------------------------
9 from qt_compat
import qt_config
12 from PyQt5
.QtCore
import Qt
, QRectF
, QPointF
, pyqtSignal
13 from PyQt5
.QtGui
import QColor
, QCursor
, QFont
, QPen
, QPainter
14 from PyQt5
.QtWidgets
import (
19 QGraphicsSimpleTextItem
,
26 from PyQt6
.QtCore
import Qt
, QRectF
, QPointF
, pyqtSignal
27 from PyQt6
.QtGui
import QColor
, QCursor
, QFont
, QPen
, QPainter
28 from PyQt6
.QtWidgets
import (
33 QGraphicsSimpleTextItem
,
40 # ------------------------------------------------------------------------------------------------------------
43 #from carla_shared import *
45 # ------------------------------------------------------------------------------------------------------------
46 # MIDI definitions, copied from CarlaMIDI.h
48 MAX_MIDI_CHANNELS
= 16
51 MAX_MIDI_CONTROL
= 120 # 0x77
53 MIDI_STATUS_BIT
= 0xF0
54 MIDI_CHANNEL_BIT
= 0x0F
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)
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
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 # ---------------------------------------------------------------------------------------------------------------------
95 class NoteExpander(QGraphicsRectItem
):
96 def __init__(self
, length
, height
, parent
):
97 QGraphicsRectItem
.__init
__(self
, 0, 0, length
, height
, parent
)
99 self
.orig_brush
= QColor(0, 0, 0, 0)
100 self
.hover_brush
= QColor(200, 200, 200)
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
)
117 def mouseReleaseEvent(self
, event
):
118 QGraphicsRectItem
.mouseReleaseEvent(self
, event
)
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
)
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
144 self
.piano
= self
.scene
147 self
.hovering
= False
148 self
.moving_diff
= (0,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
)
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
)
169 self
.setBrush(self
.hover_brush
)
171 self
.setBrush(self
.orig_brush
)
172 QGraphicsRectItem
.paint(self
, painter
, paint_option
, widget
)
174 def hoverEnterEvent(self
, event
):
175 QGraphicsRectItem
.hoverEnterEvent(self
, event
)
178 self
.setCursor(QCursor(Qt
.OpenHandCursor
))
180 def hoverLeaveEvent(self
, event
):
181 QGraphicsRectItem
.hoverLeaveEvent(self
, event
)
182 self
.hovering
= False
186 def mousePressEvent(self
, event
):
187 QGraphicsRectItem
.mousePressEvent(self
, event
)
189 self
.moving_diff
= (0,0)
191 self
.setCursor(QCursor(Qt
.ClosedHandCursor
))
192 self
.setSelected(True)
194 def mouseMoveEvent(self
, event
):
197 def mouseReleaseEvent(self
, event
):
198 QGraphicsRectItem
.mouseReleaseEvent(self
, event
)
200 self
.moving_diff
= (0,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())
212 if self
.front
.stretch
:
213 self
.expand(self
.front
, offset
)
214 self
.updateNoteInfo(self
.scenePos().x(), self
.scenePos().y())
219 pos
= self
.scenePos() + offset
+ QPointF(self
.moving_diff
[0],self
.moving_diff
[1])
220 pos
= piano
.enforce_bounds(pos
)
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)
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
):
241 width
= rect
.right() + self
.expand_diff
243 if rectItem
== self
.back
:
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
252 new_w
= piano
.snap(width
) - 2.75
253 if new_w
+ self
.scenePos().x() >= max_x
:
254 self
.moving_diff
= (0,0)
260 new_w
= piano
.snap(width
+2.75) - 2.75
262 new_w
= piano
.snap_value
263 self
.moving_diff
= (0,0)
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)
274 print(new_x
, new_w
, diff
)
277 self
.expand_diff
= width
- new_w
278 self
.back
.setPos(new_w
- 5, 0)
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()),
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:
299 elif 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)
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
)
318 self
.piano
= self
.scene
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
)
338 self
.orig_brush
= self
.brush()
339 self
.setBrush(self
.hover_brush
)
341 def hoverLeaveEvent(self
, event
):
342 QGraphicsRectItem
.hoverLeaveEvent(self
, event
)
344 self
.setBrush(self
.click_brush
if self
.pressed
else self
.orig_brush
)
346 def mousePressEvent(self
, event
):
347 QGraphicsRectItem
.mousePressEvent(self
, event
)
349 self
.setBrush(self
.click_brush
)
350 self
.piano().noteclicked
.emit(self
.note
, True)
352 def mouseReleaseEvent(self
, event
):
353 QGraphicsRectItem
.mouseReleaseEvent(self
, event
)
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
):
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))
375 self
.removed_notes
= []
376 self
.selected_notes
= []
379 self
.marquee_select
= False
380 self
.marquee_rect
= 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()
398 self
.note_height
= 10
399 self
.start_octave
= -2
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
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
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
428 self
.play_head
= None
431 self
.default_length
= 1. / self
.grid_div
434 # -------------------------------------------------------------------------
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
451 def setMeasures(self
, measures
):
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
460 def setDefaultLength(self
, length
):
461 v
= list(map(float, length
.split('/')))
463 self
.default_length
= v
[0] if len(v
) == 1 else v
[0] / v
[1]
464 pos
= self
.enforce_bounds(self
.last_mouse_pos
)
466 self
.makeGhostNote(pos
.x(), pos
.y())
468 def setGridDiv(self
, div
=None):
469 if not div
: div
= self
.quantize_val
471 val
= list(map(int, div
.split('/')))
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
)
482 def setQuantize(self
, value
):
483 val
= list(map(float, value
.split('/')))
485 self
.quantize(val
[0])
486 self
.quantize_val
= value
488 self
.quantize(val
[0] / val
[1])
489 self
.quantize_val
= value
491 # -------------------------------------------------------------------------
494 def keyPressEvent(self
, event
):
495 QGraphicsScene
.keyPressEvent(self
, event
)
497 if event
.key() == Qt
.Key_Escape
:
498 QApplication
.instance().closeAllWindows()
501 if event
.key() == Qt
.Key_F
:
502 if not self
.insert_mode
:
503 # turn off velocity mode
504 self
.velocity_mode
= False
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')
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')
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
541 has_unselected
= False
545 for note
in self
.notes
:
546 note
.setSelected(True)
547 self
.selected_notes
= self
.notes
[:]
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]])
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
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
580 keep_selection
= all(note
in self
.selected_notes
for note
in clicked_notes
)
582 for note
in self
.selected_notes
:
583 note
.setSelected(True)
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
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
:
605 self
.place_ghost
= True
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
:
621 pos
= self
.enforce_bounds(self
.last_mouse_pos
)
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
631 min_x
= self
.ghost_rect
.x() + self
.ghost_rect_orig_width
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
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
)
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
)
673 if event
.buttons() != Qt
.LeftButton
:
676 if self
.velocity_mode
:
677 for note
in self
.selected_notes
:
678 note
.updateVelocity(event
)
682 for note
in self
.selected_notes
:
683 if note
.back
.stretch
:
686 for note
in self
.selected_notes
:
687 if note
.front
.stretch
:
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
702 if self
.marquee_select
:
703 self
.marquee_select
= False
704 self
.removeItem(self
.marquee
)
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
)
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 # -------------------------------------------------------------------------
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
)
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
)
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
754 key
= PianoKeyItem(piano_keys_width
/1.4, self
.note_height
, note
, self
.piano
)
755 key
.setBrush(QColor(0, 0, 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))
771 label
= QGraphicsSimpleTextItem('{}{}'.format(labels
[j
- 1], self
.end_octave
- i
+ 1), key
)
773 label
.setFont(piano_label
)
774 self
.piano_keys
.append(key
)
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))
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
)
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
)
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
= []
822 self
.place_ghost
= False
823 if self
.ghost_note
is not None:
824 self
.removeItem(self
.ghost_note
)
825 self
.ghost_note
= None
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
))
852 self
.views()[0].setSceneRect(self
.itemsBoundingRect())
854 def clearNotes(self
):
857 self
.removed_notes
= []
858 self
.selected_notes
= []
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
)
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
)
909 # -------------------------------------------------------------------------
912 def frange(self
, x
, y
, 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):
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:
933 elif self
.ghost_vel
> 127:
936 m_width
= self
.ghost_rect
.x() + self
.ghost_rect_orig_width
937 if m_pos
.x() < 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
):
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
)
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()
988 self
.setAlignment(Qt
.AlignLeft
)
989 self
.o_transform
= self
.transform()
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)
1010 self
.setFixedSize(30,20)
1012 def paintEvent(self
, event
):
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))
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
1031 # ------------------------------------------------------------------------------------------------------------