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