|
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
-
- # A piano roll viewer/editor
- # Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
- # Copyright (C) 2014-2015 Perry Nguyen
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License as
- # published by the Free Software Foundation; either version 2 of
- # the License, or any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # For a full copy of the GNU General Public License see the doc/GPL.txt file.
-
- # ------------------------------------------------------------------------------------------------------------
- # Imports (Global)
-
- from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
- from PyQt5.QtGui import QColor, QCursor, QFont, QPen, QPainter, QTransform
- from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
- from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
- from PyQt5.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
-
- # ------------------------------------------------------------------------------------------------------------
- # Imports (Custom)
-
- from carla_shared import *
-
- # ------------------------------------------------------------------------------------------------------------
- # MIDI definitions, copied from CarlaMIDI.h
-
- MAX_MIDI_CHANNELS = 16
- MAX_MIDI_NOTE = 128
- MAX_MIDI_VALUE = 128
- MAX_MIDI_CONTROL = 120 # 0x77
-
- MIDI_STATUS_BIT = 0xF0
- MIDI_CHANNEL_BIT = 0x0F
-
- # MIDI Messages List
- MIDI_STATUS_NOTE_OFF = 0x80 # note (0-127), velocity (0-127)
- MIDI_STATUS_NOTE_ON = 0x90 # note (0-127), velocity (0-127)
- MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
- MIDI_STATUS_CONTROL_CHANGE = 0xB0 # see 'Control Change Messages List'
- MIDI_STATUS_PROGRAM_CHANGE = 0xC0 # program (0-127), none
- MIDI_STATUS_CHANNEL_PRESSURE = 0xD0 # pressure (0-127), none
- MIDI_STATUS_PITCH_WHEEL_CONTROL = 0xE0 # LSB (0-127), MSB (0-127)
-
- # MIDI Message type
- def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status < MIDI_STATUS_BIT
- def MIDI_IS_SYSTEM_MESSAGE(status): return status >= MIDI_STATUS_BIT and status <= 0xFF
- def MIDI_IS_OSC_MESSAGE(status): return status == '/' or status == '#'
-
- # MIDI Channel message type
- def MIDI_IS_STATUS_NOTE_OFF(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
- def MIDI_IS_STATUS_NOTE_ON(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
- def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
- def MIDI_IS_STATUS_CONTROL_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
- def MIDI_IS_STATUS_PROGRAM_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
- def MIDI_IS_STATUS_CHANNEL_PRESSURE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
- def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL
-
- # MIDI Utils
- def MIDI_GET_STATUS_FROM_DATA(data): return data[0] & MIDI_STATUS_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
- def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0
-
- # ---------------------------------------------------------------------------------------------------------------------
- # Graphics Items
-
- class NoteExpander(QGraphicsRectItem):
- def __init__(self, length, height, parent):
- QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
- self.parent = parent
- self.orig_brush = QColor(0, 0, 0, 0)
- self.hover_brush = QColor(200, 200, 200)
- self.stretch = False
-
- self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- self.setPen(QPen(QColor(0,0,0,0)))
-
- def paint(self, painter, option, widget=None):
- paint_option = option
- paint_option.state &= ~QStyle.State_Selected
- QGraphicsRectItem.paint(self, painter, paint_option, widget)
-
- def mousePressEvent(self, event):
- QGraphicsRectItem.mousePressEvent(self, event)
- self.stretch = True
-
- def mouseReleaseEvent(self, event):
- QGraphicsRectItem.mouseReleaseEvent(self, event)
- self.stretch = False
-
- def hoverEnterEvent(self, event):
- QGraphicsRectItem.hoverEnterEvent(self, event)
- self.setCursor(QCursor(Qt.SizeHorCursor))
- self.setBrush(self.hover_brush)
-
- def hoverLeaveEvent(self, event):
- QGraphicsRectItem.hoverLeaveEvent(self, event)
- self.unsetCursor()
- self.setBrush(self.orig_brush)
-
- # ---------------------------------------------------------------------------------------------------------------------
-
- class NoteItem(QGraphicsRectItem):
- '''a note on the pianoroll sequencer'''
- def __init__(self, height, length, note_info):
- QGraphicsRectItem.__init__(self, 0, 0, length, height)
-
- self.orig_brush = QColor(note_info[3], 0, 0)
- self.hover_brush = QColor(note_info[3] + 98, 200, 100)
- self.select_brush = QColor(note_info[3] + 98, 100, 100)
-
- self.note = note_info
- self.length = length
- self.piano = self.scene
-
- self.pressed = False
- self.hovering = False
- self.moving_diff = (0,0)
- self.expand_diff = 0
-
- self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- self.setPen(QPen(QColor(0,0,0,0)))
- self.setBrush(self.orig_brush)
-
- l = 5
- self.front = NoteExpander(l, height, self)
- self.back = NoteExpander(l, height, self)
- self.back.setPos(length - l, 0)
-
- def paint(self, painter, option, widget=None):
- paint_option = option
- paint_option.state &= ~QStyle.State_Selected
- if self.isSelected():
- self.setBrush(self.select_brush)
- elif self.hovering:
- self.setBrush(self.hover_brush)
- else:
- self.setBrush(self.orig_brush)
- QGraphicsRectItem.paint(self, painter, paint_option, widget)
-
- def hoverEnterEvent(self, event):
- QGraphicsRectItem.hoverEnterEvent(self, event)
- self.hovering = True
- self.update()
- self.setCursor(QCursor(Qt.OpenHandCursor))
-
- def hoverLeaveEvent(self, event):
- QGraphicsRectItem.hoverLeaveEvent(self, event)
- self.hovering = False
- self.unsetCursor()
- self.update()
-
- def mousePressEvent(self, event):
- QGraphicsRectItem.mousePressEvent(self, event)
- self.pressed = True
- self.moving_diff = (0,0)
- self.expand_diff = 0
- self.setCursor(QCursor(Qt.ClosedHandCursor))
- self.setSelected(True)
-
- def mouseMoveEvent(self, event):
- event.ignore()
-
- def mouseReleaseEvent(self, event):
- QGraphicsRectItem.mouseReleaseEvent(self, event)
- self.pressed = False
- self.moving_diff = (0,0)
- self.expand_diff = 0
- self.setCursor(QCursor(Qt.OpenHandCursor))
-
- def moveEvent(self, event):
- offset = event.scenePos() - event.lastScenePos()
-
- if self.back.stretch:
- self.expand(self.back, offset)
- self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
- return
-
- if self.front.stretch:
- self.expand(self.front, offset)
- self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
- return
-
- piano = self.piano()
-
- pos = self.scenePos() + offset + QPointF(self.moving_diff[0],self.moving_diff[1])
- pos = piano.enforce_bounds(pos)
- pos_x = pos.x()
- pos_y = pos.y()
- width = self.rect().width()
- if pos_x + width > piano.grid_width + piano.piano_width:
- pos_x = piano.grid_width + piano.piano_width - width
- pos_sx, pos_sy = piano.snap(pos_x, pos_y)
-
- if pos_sx + width > piano.grid_width + piano.piano_width:
- self.moving_diff = (0,0)
- self.expand_diff = 0
- return
-
- self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
- self.setPos(pos_sx, pos_sy)
-
- self.updateNoteInfo(pos_sx, pos_sy)
-
- def expand(self, rectItem, offset):
- rect = self.rect()
- piano = self.piano()
- width = rect.right() + self.expand_diff
-
- if rectItem == self.back:
- width += offset.x()
- max_x = piano.grid_width + piano.piano_width
- if width + self.scenePos().x() >= max_x:
- width = max_x - self.scenePos().x() - 1
- elif piano.snap_value and width < piano.snap_value:
- width = piano.snap_value
- elif width < 10:
- width = 10
- new_w = piano.snap(width) - 2.75
- if new_w + self.scenePos().x() >= max_x:
- self.moving_diff = (0,0)
- self.expand_diff = 0
- return
-
- else:
- width -= offset.x()
- new_w = piano.snap(width+2.75) - 2.75
- if new_w <= 0:
- new_w = piano.snap_value
- self.moving_diff = (0,0)
- self.expand_diff = 0
- return
- diff = rect.right() - new_w
- if diff: # >= piano.snap_value:
- new_x = self.scenePos().x() + diff
- if new_x < piano.piano_width:
- new_x = piano.piano_width
- self.moving_diff = (0,0)
- self.expand_diff = 0
- return
- print(new_x, new_w, diff)
- self.setX(new_x)
-
- self.expand_diff = width - new_w
- self.back.setPos(new_w - 5, 0)
- rect.setRight(new_w)
- self.setRect(rect)
-
- def updateNoteInfo(self, pos_x, pos_y):
- note_info = (self.piano().get_note_num_from_y(pos_y),
- self.piano().get_note_start_from_x(pos_x),
- self.piano().get_note_length_from_x(self.rect().width()),
- self.note[3])
- if self.note != note_info:
- self.piano().move_note(self.note, note_info)
- self.note = note_info
-
- def updateVelocity(self, event):
- offset = event.scenePos().x() - event.lastScenePos().x()
- offset = int(offset/5)
-
- note_info = self.note[:]
- note_info[3] += offset
- if note_info[3] > 127:
- note_info[3] = 127
- elif note_info[3] < 0:
- note_info[3] = 0
- if self.note != note_info:
- self.orig_brush = QColor(note_info[3], 0, 0)
- self.hover_brush = QColor(note_info[3] + 98, 200, 100)
- self.select_brush = QColor(note_info[3] + 98, 100, 100)
- self.update()
- self.piano().move_note(self.note, note_info)
- self.note = note_info
-
- # ---------------------------------------------------------------------------------------------------------------------
-
- class PianoKeyItem(QGraphicsRectItem):
- def __init__(self, width, height, note, parent):
- QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
-
- self.width = width
- self.height = height
- self.note = note
- self.piano = self.scene
- self.hovered = False
- self.pressed = False
-
- self.click_brush = QColor(255, 100, 100)
- self.hover_brush = QColor(200, 0, 0)
- self.orig_brush = None
-
- self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setPen(QPen(QColor(0,0,0,80)))
-
- def paint(self, painter, option, widget=None):
- paint_option = option
- paint_option.state &= ~QStyle.State_Selected
- QGraphicsRectItem.paint(self, painter, paint_option, widget)
-
- def hoverEnterEvent(self, event):
- QGraphicsRectItem.hoverEnterEvent(self, event)
- self.hovered = True
- self.orig_brush = self.brush()
- self.setBrush(self.hover_brush)
-
- def hoverLeaveEvent(self, event):
- QGraphicsRectItem.hoverLeaveEvent(self, event)
- self.hovered = False
- self.setBrush(self.click_brush if self.pressed else self.orig_brush)
-
- def mousePressEvent(self, event):
- QGraphicsRectItem.mousePressEvent(self, event)
- self.pressed = True
- self.setBrush(self.click_brush)
- self.piano().noteclicked.emit(self.note, True)
-
- def mouseReleaseEvent(self, event):
- QGraphicsRectItem.mouseReleaseEvent(self, event)
- self.pressed = False
- self.setBrush(self.hover_brush if self.hovered else self.orig_brush)
- self.piano().noteclicked.emit(self.note, False)
-
- # ---------------------------------------------------------------------------------------------------------------------
-
- class PianoRoll(QGraphicsScene):
- '''the piano roll'''
-
- noteclicked = pyqtSignal(int,bool)
- midievent = pyqtSignal(list)
- measureupdate = pyqtSignal(int)
- modeupdate = pyqtSignal(str)
-
- default_ghost_vel = 100
-
- def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
- QGraphicsScene.__init__(self)
- self.setBackgroundBrush(QColor(50, 50, 50))
-
- self.notes = []
- self.removed_notes = []
- self.selected_notes = []
- self.piano_keys = []
-
- self.marquee_select = False
- self.marquee_rect = None
- self.marquee = None
-
- self.ghost_note = None
- self.ghost_rect = None
- self.ghost_rect_orig_width = None
- self.ghost_vel = self.default_ghost_vel
-
- self.ignore_mouse_events = False
- self.insert_mode = False
- self.velocity_mode = False
- self.place_ghost = False
- self.last_mouse_pos = QPointF()
-
- ## dimensions
- self.padding = 2
-
- ## piano dimensions
- self.note_height = 10
- self.start_octave = -2
- self.end_octave = 8
- self.notes_in_octave = 12
- self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1
- self.piano_height = self.note_height * self.total_notes
- self.octave_height = self.notes_in_octave * self.note_height
-
- self.piano_width = 34
-
- ## height
- self.header_height = 20
- self.total_height = self.piano_height - self.note_height + self.header_height
- #not sure why note_height is subtracted
-
- ## width
- self.full_note_width = 250 # i.e. a 4/4 note
- self.snap_value = None
- self.quantize_val = quantize_val
-
- ### dummy vars that will be changed
- self.time_sig = (0,0)
- self.measure_width = 0
- self.num_measures = 0
- self.max_note_length = 0
- self.grid_width = 0
- self.value_width = 0
- self.grid_div = 0
- self.piano = None
- self.header = None
- self.play_head = None
-
- self.setGridDiv()
- self.default_length = 1. / self.grid_div
-
-
- # -------------------------------------------------------------------------
- # Callbacks
-
- def movePlayHead(self, transportInfo):
- ticksPerBeat = transportInfo['ticksPerBeat']
- max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures
- cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick']
- frac = (cur_tick % max_ticks) / max_ticks
- self.play_head.setPos(QPointF(frac * self.grid_width, 0))
-
- def setTimeSig(self, time_sig):
- self.time_sig = time_sig
- self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
- self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
- self.grid_width = self.measure_width * self.num_measures
- self.setGridDiv()
-
- def setMeasures(self, measures):
- #try:
- self.num_measures = float(measures)
- self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
- self.grid_width = self.measure_width * self.num_measures
- self.refreshScene()
- #except:
- #pass
-
- def setDefaultLength(self, length):
- v = list(map(float, length.split('/')))
- if len(v) < 3:
- self.default_length = v[0] if len(v) == 1 else v[0] / v[1]
- pos = self.enforce_bounds(self.last_mouse_pos)
- if self.insert_mode:
- self.makeGhostNote(pos.x(), pos.y())
-
- def setGridDiv(self, div=None):
- if not div: div = self.quantize_val
- try:
- val = list(map(int, div.split('/')))
- if len(val) < 3:
- self.quantize_val = div
- self.grid_div = val[0] if len(val)==1 else val[1]
- self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
- self.setQuantize(div)
-
- self.refreshScene()
- except ValueError:
- pass
-
- def setQuantize(self, value):
- val = list(map(float, value.split('/')))
- if len(val) == 1:
- self.quantize(val[0])
- self.quantize_val = value
- elif len(val) == 2:
- self.quantize(val[0] / val[1])
- self.quantize_val = value
-
- # -------------------------------------------------------------------------
- # Event Callbacks
-
- def keyPressEvent(self, event):
- QGraphicsScene.keyPressEvent(self, event)
-
- if event.key() == Qt.Key_Escape:
- QApplication.instance().closeAllWindows()
- return
-
- if event.key() == Qt.Key_F:
- if not self.insert_mode:
- # turn off velocity mode
- self.velocity_mode = False
- # enable insert mode
- self.insert_mode = True
- self.place_ghost = False
- self.makeGhostNote(self.last_mouse_pos.x(), self.last_mouse_pos.y())
- self.modeupdate.emit('insert_mode')
- else:
- # turn off insert mode
- self.insert_mode = False
- self.place_ghost = False
- if self.ghost_note is not None:
- self.removeItem(self.ghost_note)
- self.ghost_note = None
- self.modeupdate.emit('')
-
- elif event.key() == Qt.Key_D:
- if not self.velocity_mode:
- # turn off insert mode
- self.insert_mode = False
- self.place_ghost = False
- if self.ghost_note is not None:
- self.removeItem(self.ghost_note)
- self.ghost_note = None
- # enable velocity mode
- self.velocity_mode = True
- self.modeupdate.emit('velocity_mode')
- else:
- # turn off velocity mode
- self.velocity_mode = False
- self.modeupdate.emit('')
-
- elif event.key() == Qt.Key_A:
- for note in self.notes:
- if not note.isSelected():
- has_unselected = True
- break
- else:
- has_unselected = False
-
- # select all notes
- if has_unselected:
- for note in self.notes:
- note.setSelected(True)
- self.selected_notes = self.notes[:]
- # unselect all
- else:
- for note in self.notes:
- note.setSelected(False)
- self.selected_notes = []
-
- elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
- # remove selected notes from our notes list
- self.notes = [note for note in self.notes if note not in self.selected_notes]
- # delete the selected notes
- for note in self.selected_notes:
- self.removeItem(note)
- self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
- del note
- self.selected_notes = []
-
- def mousePressEvent(self, event):
- QGraphicsScene.mousePressEvent(self, event)
-
- # mouse click on left-side piano area
- if self.piano.contains(event.scenePos()):
- self.ignore_mouse_events = True
- return
-
- clicked_notes = []
-
- for note in self.notes:
- if note.pressed or note.back.stretch or note.front.stretch:
- clicked_notes.append(note)
-
- # mouse click on existing notes
- if clicked_notes:
- keep_selection = all(note in self.selected_notes for note in clicked_notes)
- if keep_selection:
- for note in self.selected_notes:
- note.setSelected(True)
- return
-
- for note in self.selected_notes:
- if note not in clicked_notes:
- note.setSelected(False)
- for note in clicked_notes:
- if note not in self.selected_notes:
- note.setSelected(True)
-
- self.selected_notes = clicked_notes
- return
-
- # mouse click on empty area (no note selected)
- for note in self.selected_notes:
- note.setSelected(False)
- self.selected_notes = []
-
- if event.button() != Qt.LeftButton:
- return
-
- if self.insert_mode:
- self.place_ghost = True
- else:
- self.marquee_select = True
- self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
- self.marquee = QGraphicsRectItem(self.marquee_rect)
- self.marquee.setBrush(QColor(255, 255, 255, 100))
- self.addItem(self.marquee)
-
- def mouseMoveEvent(self, event):
- QGraphicsScene.mouseMoveEvent(self, event)
-
- self.last_mouse_pos = event.scenePos()
-
- if self.ignore_mouse_events:
- return
-
- pos = self.enforce_bounds(self.last_mouse_pos)
-
- if self.insert_mode:
- if self.ghost_note is None:
- self.makeGhostNote(pos.x(), pos.y())
- max_x = self.grid_width + self.piano_width
-
- # placing note, only width needs updating
- if self.place_ghost:
- pos_x = pos.x()
- min_x = self.ghost_rect.x() + self.ghost_rect_orig_width
- if pos_x < min_x:
- pos_x = min_x
- new_x = self.snap(pos_x)
- self.ghost_rect.setRight(new_x)
- self.ghost_note.setRect(self.ghost_rect)
- #self.adjust_note_vel(event)
-
- # ghostnote following mouse around
- else:
- pos_x = pos.x()
- if pos_x + self.ghost_rect.width() >= max_x:
- pos_x = max_x - self.ghost_rect.width()
- elif pos_x > self.piano_width + self.ghost_rect.width()*3/4:
- pos_x -= self.ghost_rect.width()/2
- new_x, new_y = self.snap(pos_x, pos.y())
- self.ghost_rect.moveTo(new_x, new_y)
- self.ghost_note.setRect(self.ghost_rect)
- return
-
- if self.marquee_select:
- marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
- if marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() < pos.y():
- self.marquee_rect.setBottomRight(pos)
- elif marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() > pos.y():
- self.marquee_rect.setTopRight(pos)
- elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() < pos.y():
- self.marquee_rect.setBottomLeft(pos)
- elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() > pos.y():
- self.marquee_rect.setTopLeft(pos)
- self.marquee.setRect(self.marquee_rect)
-
- for note in self.selected_notes:
- note.setSelected(False)
- self.selected_notes = []
-
- for item in self.collidingItems(self.marquee):
- if item in self.notes:
- item.setSelected(True)
- self.selected_notes.append(item)
- return
-
- if event.buttons() != Qt.LeftButton:
- return
-
- if self.velocity_mode:
- for note in self.selected_notes:
- note.updateVelocity(event)
- return
-
- x = y = False
- for note in self.selected_notes:
- if note.back.stretch:
- x = True
- break
- for note in self.selected_notes:
- if note.front.stretch:
- y = True
- break
- for note in self.selected_notes:
- note.back.stretch = x
- note.front.stretch = y
- note.moveEvent(event)
-
- def mouseReleaseEvent(self, event):
- QGraphicsScene.mouseReleaseEvent(self, event)
-
- if self.ignore_mouse_events:
- self.ignore_mouse_events = False
- return
-
- if self.marquee_select:
- self.marquee_select = False
- self.removeItem(self.marquee)
- self.marquee = None
-
- if self.insert_mode and self.place_ghost:
- self.place_ghost = False
- note_start = self.get_note_start_from_x(self.ghost_rect.x())
- note_num = self.get_note_num_from_y(self.ghost_rect.y())
- note_length = self.get_note_length_from_x(self.ghost_rect.width())
- note = self.drawNote(note_num, note_start, note_length, self.ghost_vel)
- note.setSelected(True)
- self.selected_notes.append(note)
- self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
- pos = self.enforce_bounds(self.last_mouse_pos)
- pos_x = pos.x()
- if pos_x > self.piano_width + self.ghost_rect.width()*3/4:
- pos_x -= self.ghost_rect.width()/2
- self.makeGhostNote(pos_x, pos.y())
-
- for note in self.selected_notes:
- note.back.stretch = False
- note.front.stretch = False
-
- # -------------------------------------------------------------------------
- # Internal Functions
-
- def drawHeader(self):
- self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
- #self.header.setZValue(1.0)
- self.header.setPos(self.piano_width, 0)
- self.addItem(self.header)
-
- def drawPiano(self):
- piano_keys_width = self.piano_width - self.padding
- labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
- black_notes = (2,4,6,9,11)
- piano_label = QFont()
- piano_label.setPointSize(6)
- self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
- self.piano.setPos(0, self.header_height)
- self.addItem(self.piano)
-
- key = PianoKeyItem(piano_keys_width, self.note_height, 78, self.piano)
- label = QGraphicsSimpleTextItem('C9', key)
- label.setPos(18, 1)
- label.setFont(piano_label)
- key.setBrush(QColor(255, 255, 255))
- for i in range(self.end_octave - self.start_octave, 0, -1):
- for j in range(self.notes_in_octave, 0, -1):
- note = (self.end_octave - i + 3) * 12 - j
- if j in black_notes:
- key = PianoKeyItem(piano_keys_width/1.4, self.note_height, note, self.piano)
- key.setBrush(QColor(0, 0, 0))
- key.setZValue(1.0)
- key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
- elif (j - 1) and (j + 1) in black_notes:
- key = PianoKeyItem(piano_keys_width, self.note_height * 2, note, self.piano)
- key.setBrush(QColor(255, 255, 255))
- key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
- elif (j - 1) in black_notes:
- key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
- key.setBrush(QColor(255, 255, 255))
- key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
- elif (j + 1) in black_notes:
- key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
- key.setBrush(QColor(255, 255, 255))
- key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
- if j == 12:
- label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i + 1), key)
- label.setPos(18, 6)
- label.setFont(piano_label)
- self.piano_keys.append(key)
-
- def drawGrid(self):
- black_notes = [2,4,6,9,11]
- scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
- scale_bar.setPos(self.piano_width, 0)
- scale_bar.setBrush(QColor(100,100,100))
- clearpen = QPen(QColor(0,0,0,0))
- for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
- for j in range(self.notes_in_octave, 0, -1):
- scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
- scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
- scale_bar.setPen(clearpen)
- if j not in black_notes:
- scale_bar.setBrush(QColor(120,120,120))
- else:
- scale_bar.setBrush(QColor(100,100,100))
-
- measure_pen = QPen(QColor(0, 0, 0, 120), 3)
- half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
- line_pen = QPen(QColor(0, 0, 0, 40))
- for i in range(0, int(self.num_measures) + 1):
- measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
- measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
- measure.setPen(measure_pen)
- if i < self.num_measures:
- number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
- number.setPos(self.measure_width * i + 5, 2)
- number.setBrush(Qt.white)
- for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
- line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
- line.setZValue(1.0)
- line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
- if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
- line.setPen(half_measure_pen)
- else:
- line.setPen(line_pen)
-
- def drawPlayHead(self):
- self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
- self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
- self.play_head.setZValue(1.)
- self.addItem(self.play_head)
-
- def refreshScene(self):
- list(map(self.removeItem, self.notes))
- self.selected_notes = []
- self.piano_keys = []
- self.place_ghost = False
- if self.ghost_note is not None:
- self.removeItem(self.ghost_note)
- self.ghost_note = None
- self.clear()
- self.drawPiano()
- self.drawHeader()
- self.drawGrid()
- self.drawPlayHead()
-
- for note in self.notes[:]:
- if note.note[1] >= (self.num_measures * self.time_sig[0]):
- self.notes.remove(note)
- self.removed_notes.append(note)
- #self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
- elif note.note[2] > self.max_note_length:
- new_note = note.note[:]
- new_note[2] = self.max_note_length
- self.notes.remove(note)
- self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
- self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
- self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
-
- for note in self.removed_notes[:]:
- if note.note[1] < (self.num_measures * self.time_sig[0]):
- self.removed_notes.remove(note)
- self.notes.append(note)
-
- list(map(self.addItem, self.notes))
- if self.views():
- self.views()[0].setSceneRect(self.itemsBoundingRect())
-
- def clearNotes(self):
- self.clear()
- self.notes = []
- self.removed_notes = []
- self.selected_notes = []
- self.drawPiano()
- self.drawHeader()
- self.drawGrid()
-
- def makeGhostNote(self, pos_x, pos_y):
- """creates the ghostnote that is placed on the scene before the real one is."""
- if self.ghost_note is not None:
- self.removeItem(self.ghost_note)
- length = self.full_note_width * self.default_length
- pos_x, pos_y = self.snap(pos_x, pos_y)
- self.ghost_vel = self.default_ghost_vel
- self.ghost_rect = QRectF(pos_x, pos_y, length, self.note_height)
- self.ghost_rect_orig_width = self.ghost_rect.width()
- self.ghost_note = QGraphicsRectItem(self.ghost_rect)
- self.ghost_note.setBrush(QColor(230, 221, 45, 100))
- self.addItem(self.ghost_note)
-
- def drawNote(self, note_num, note_start, note_length, note_velocity, add=True):
- """
- note_num: midi number, 0 - 127
- note_start: 0 - (num_measures * time_sig[0]) so this is in beats
- note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
- note_velocity: 0 - 127
- """
-
- info = [note_num, note_start, note_length, note_velocity]
-
- if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
- #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
- while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
- self.setMeasures(self.num_measures+1)
- self.measureupdate.emit(self.num_measures)
- self.refreshScene()
-
- x_start = self.get_note_x_start(note_start)
- if note_length > self.max_note_length:
- note_length = self.max_note_length + 0.25
- x_length = self.get_note_x_length(note_length)
- y_pos = self.get_note_y_pos(note_num)
-
- note = NoteItem(self.note_height, x_length, info)
- note.setPos(x_start, y_pos)
-
- self.notes.append(note)
-
- if add:
- self.addItem(note)
-
- return note
-
- # -------------------------------------------------------------------------
- # Helper Functions
-
- def frange(self, x, y, t):
- while x < y:
- yield x
- x += t
-
- def quantize(self, value):
- self.snap_value = float(self.full_note_width) * value if value else None
-
- def snap(self, pos_x, pos_y = None):
- if self.snap_value:
- pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) * self.snap_value + self.piano_width
- if pos_y is not None:
- pos_y = int((pos_y - self.header_height) / self.note_height) * self.note_height + self.header_height
- return (pos_x, pos_y) if pos_y is not None else pos_x
-
- def adjust_note_vel(self, event):
- m_pos = event.scenePos()
- #bind velocity to vertical mouse movement
- self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
- if self.ghost_vel < 0:
- self.ghost_vel = 0
- elif self.ghost_vel > 127:
- self.ghost_vel = 127
-
- m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
- if m_pos.x() < m_width:
- m_pos.setX(m_width)
- m_new_x = self.snap(m_pos.x())
- self.ghost_rect.setRight(m_new_x)
- self.ghost_note.setRect(self.ghost_rect)
-
- def enforce_bounds(self, pos):
- pos = QPointF(pos)
- if pos.x() < self.piano_width:
- pos.setX(self.piano_width)
- elif pos.x() >= self.grid_width + self.piano_width:
- pos.setX(self.grid_width + self.piano_width - 1)
- if pos.y() < self.header_height + self.padding:
- pos.setY(self.header_height + self.padding)
- return pos
-
- def get_note_start_from_x(self, note_x):
- return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
-
- def get_note_x_start(self, note_start):
- return self.piano_width + (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
-
- def get_note_x_length(self, note_length):
- return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
-
- def get_note_length_from_x(self, note_x):
- return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width * note_x
-
- def get_note_y_pos(self, note_num):
- return self.header_height + self.note_height * (self.total_notes - note_num - 1)
-
- def get_note_num_from_y(self, note_y_pos):
- return -(int((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
-
- def move_note(self, old_note, new_note):
- self.midievent.emit(["midievent-remove", old_note[0], old_note[1], old_note[2], old_note[3]])
- self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
-
- # ------------------------------------------------------------------------------------------------------------
-
- class PianoRollView(QGraphicsView):
- def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
- QGraphicsView.__init__(self, parent)
- self.piano = PianoRoll(time_sig, num_measures, quantize_val)
- self.setScene(self.piano)
- #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
- x = 0 * self.sceneRect().width() + self.sceneRect().left()
- y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
- self.centerOn(x, y)
-
- self.setAlignment(Qt.AlignLeft)
- self.o_transform = self.transform()
- self.zoom_x = 1
- self.zoom_y = 1
-
- def setZoomX(self, scale_x):
- self.setTransform(self.o_transform)
- self.zoom_x = 1 + scale_x / float(99) * 2
- self.scale(self.zoom_x, self.zoom_y)
-
- def setZoomY(self, scale_y):
- self.setTransform(self.o_transform)
- self.zoom_y = 1 + scale_y / float(99)
- self.scale(self.zoom_x, self.zoom_y)
-
- # ------------------------------------------------------------------------------------------------------------
-
- class ModeIndicator(QWidget):
- def __init__(self, parent):
- QWidget.__init__(self, parent)
- #self.setGeometry(0, 0, 30, 20)
- self.mode = None
- self.setFixedSize(30,20)
-
- def paintEvent(self, event):
- event.accept()
-
- painter = QPainter(self)
- painter.setPen(QPen(QColor(0, 0, 0, 0)))
-
- if self.mode == 'velocity_mode':
- painter.setBrush(QColor(127, 0, 0))
- elif self.mode == 'insert_mode':
- painter.setBrush(QColor(0, 100, 127))
- else:
- painter.setBrush(QColor(0, 0, 0, 0))
-
- painter.drawRect(0, 0, 30, 20)
-
- def changeMode(self, new_mode):
- self.mode = new_mode
- self.update()
-
- # ------------------------------------------------------------------------------------------------------------
|