diff --git a/.gitignore b/.gitignore index 878392b14..f60a57a17 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ source/carla_config.py source/digitalpeakmeter.py source/ledbutton.py source/paramspinbox.py +source/pianoroll.py source/pixmapbutton.py source/pixmapdial.py source/pixmapkeyboard.py diff --git a/Makefile b/Makefile index 196f236a2..f033f9805 100644 --- a/Makefile +++ b/Makefile @@ -269,6 +269,7 @@ RES = \ bin/resources/paramspinbox.py \ bin/resources/patchcanvas.py \ bin/resources/patchcanvas_theme.py \ + bin/resources/pianoroll.py \ bin/resources/pixmapbutton.py \ bin/resources/pixmapdial.py \ bin/resources/pixmapkeyboard.py \ @@ -290,6 +291,7 @@ RES = \ bin/resources/ui_carla_settings.py \ bin/resources/ui_carla_settings_driver.py \ bin/resources/ui_inputdialog_value.py \ + bin/resources/ui_midipattern.py \ source/carla_config.py \ source/resources_rc.py @@ -333,7 +335,8 @@ UIs = \ source/ui_carla_refresh.py \ source/ui_carla_settings.py \ source/ui_carla_settings_driver.py \ - source/ui_inputdialog_value.py + source/ui_inputdialog_value.py \ + source/ui_midipattern.py UI: $(UIs) @@ -351,6 +354,7 @@ WIDGETS = \ source/digitalpeakmeter.py \ source/ledbutton.py \ source/paramspinbox.py \ + source/pianoroll.py \ source/pixmapbutton.py \ source/pixmapdial.py \ source/pixmapkeyboard.py \ @@ -593,6 +597,7 @@ endif $(LINK) $(PREFIX)/share/carla/paramspinbox.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/patchcanvas.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/patchcanvas_theme.py $(DESTDIR)$(PREFIX)/share/carla/resources/ + $(LINK) $(PREFIX)/share/carla/pianoroll.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/pixmapbutton.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/pixmapdial.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/pixmapkeyboard.py $(DESTDIR)$(PREFIX)/share/carla/resources/ @@ -614,6 +619,7 @@ endif $(LINK) $(PREFIX)/share/carla/ui_carla_settings.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/ui_carla_settings_driver.py $(DESTDIR)$(PREFIX)/share/carla/resources/ $(LINK) $(PREFIX)/share/carla/ui_inputdialog_value.py $(DESTDIR)$(PREFIX)/share/carla/resources/ + $(LINK) $(PREFIX)/share/carla/ui_midipattern.py $(DESTDIR)$(PREFIX)/share/carla/resources/ # Adjust PREFIX value in script files sed -i "s?X-PREFIX-X?$(PREFIX)?" \ diff --git a/resources/ui/midipattern.ui b/resources/ui/midipattern.ui new file mode 100644 index 000000000..f2ed241f8 --- /dev/null +++ b/resources/ui/midipattern.ui @@ -0,0 +1,499 @@ + + + MidiPatternW + + + + 0 + 0 + 755 + 436 + + + + MIDI Pattern + + + + + + + + + + 30 + 20 + + + + + 30 + 20 + + + + + + + + Time Signature: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + 3 + + + + 1/4 + + + + + 2/4 + + + + + 3/4 + + + + + 4/4 + + + + + 5/4 + + + + + 6/4 + + + + + 12/8 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + Measures: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 3 + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + 13 + + + + + 14 + + + + + 15 + + + + + 16 + + + + + 17 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + Default Length: + + + + + + + + 1/16 + + + + + 1/15 + + + + + 1/12 + + + + + 1/9 + + + + + 1/8 + + + + + 1/6 + + + + + 1/4 + + + + + 1/3 + + + + + 1/2 + + + + + 1 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + Quantize: + + + + + + + + 1/16 + + + + + 1/15 + + + + + 1/12 + + + + + 1/9 + + + + + 1/8 + + + + + 1/6 + + + + + 1/4 + + + + + 1/3 + + + + + 1/2 + + + + + 1 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + Qt::Horizontal + + + + + + + + + + + Qt::Vertical + + + + + + + + + + + + + + 0 + 0 + 755 + 20 + + + + + &File + + + + + + &Edit + + + + + + + + + + + + &Quit + + + + + true + + + &Insert Mode + + + F + + + + + true + + + &Velocity Mode + + + D + + + + + Select All + + + A + + + + + + PianoRollView + QGraphicsView +
pianoroll.h
+
+ + ModeIndicator + QWidget +
pianoroll.h
+ 1 +
+
+ + + + act_file_quit + triggered() + MidiPatternW + close() + + + -1 + -1 + + + 377 + 217 + + + + +
diff --git a/source/native-plugins/resources/midipattern-ui b/source/native-plugins/resources/midipattern-ui index 3e6ee9a47..7d165ab23 100755 --- a/source/native-plugins/resources/midipattern-ui +++ b/source/native-plugins/resources/midipattern-ui @@ -25,26 +25,22 @@ from carla_config import * # ------------------------------------------------------------------------------------------------------------ # Imports (Global) -config_UseQt5 = False - if config_UseQt5: - from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal - from PyQt5.QtGui import QColor, QFont, QPen, QPainter - from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem - from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView - from PyQt4.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle + from PyQt5.QtCore import pyqtSlot, Qt, QEvent + from PyQt5.QtGui import QKeyEvent + from PyQt5.QtWidgets import QMainWindow else: - from PyQt4.QtCore import Qt, QRectF, QPointF, pyqtSignal - from PyQt4.QtGui import QColor, QFont, QPen, QPainter - from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem - from PyQt4.QtGui import QGraphicsScene, QGraphicsView - from PyQt4.QtGui import QWidget, QLabel, QComboBox, QSlider, QHBoxLayout, QVBoxLayout, QStyle + from PyQt4.QtCore import pyqtSlot, Qt, QEvent + from PyQt4.QtGui import QKeyEvent, QMainWindow # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from carla_shared import * from carla_utils import * +from pianoroll import * + +import ui_midipattern # ------------------------------------------------------------------------------------------------------------ # Imports (ExternalUI) @@ -53,848 +49,16 @@ from carla_app import CarlaApplication from externalui import ExternalUI # ------------------------------------------------------------------------------------------------------------ -# 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.setFlag(QGraphicsItem.ItemSendsGeometryChanges) - self.setAcceptHoverEvents(True) - - clearpen = QPen(QColor(0,0,0,0)) - self.setPen(clearpen) - - self.orig_brush = QColor(0, 0, 0, 0) - self.hover_brush = QColor(200, 200, 200) - self.stretch = False - - def mousePressEvent(self, event): - QGraphicsRectItem.mousePressEvent(self, event) - self.stretch = True - - def hoverEnterEvent(self, event): - QGraphicsRectItem.hoverEnterEvent(self, event) - if self.parent.isSelected(): - self.parent.setBrush(self.parent.select_brush) - else: - self.parent.setBrush(self.parent.orig_brush) - self.setBrush(self.hover_brush) - - def hoverLeaveEvent(self, event): - QGraphicsRectItem.hoverLeaveEvent(self, event) - if self.parent.isSelected(): - self.parent.setBrush(self.parent.select_brush) - elif self.parent.hovering: - self.parent.setBrush(self.parent.hover_brush) - else: - self.parent.setBrush(self.parent.orig_brush) - 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.setFlag(QGraphicsItem.ItemIsMovable) - self.setFlag(QGraphicsItem.ItemIsSelectable) - self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) - self.setAcceptHoverEvents(True) - - clearpen = QPen(QColor(0,0,0,0)) - self.setPen(clearpen) - self.orig_brush = QColor(note_info[3], 0, 0) - self.hover_brush = QColor(note_info[3] + 100, 200, 100) - self.select_brush = QColor(note_info[3] + 100, 100, 100) - self.setBrush(self.orig_brush) - - 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 - - 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 - QGraphicsRectItem.paint(self, painter, paint_option, widget) - - def setSelected(self, boolean): - QGraphicsRectItem.setSelected(self, boolean) - if boolean: self.setBrush(self.select_brush) - else: self.setBrush(self.orig_brush) - - def hoverEnterEvent(self, event): - self.hovering = True - QGraphicsRectItem.hoverEnterEvent(self, event) - if not self.isSelected(): - self.setBrush(self.hover_brush) - - def hoverLeaveEvent(self, event): - self.hovering = False - QGraphicsRectItem.hoverLeaveEvent(self, event) - if not self.isSelected(): - self.setBrush(self.orig_brush) - elif self.isSelected(): - self.setBrush(self.select_brush) - - def mousePressEvent(self, event): - QGraphicsRectItem.mousePressEvent(self, event) - self.setSelected(True) - self.pressed = True - - def mouseMoveEvent(self, event): - pass - def moveEvent(self, event): - offset = event.scenePos() - event.lastScenePos() +class MidiPatternW(ExternalUI, QMainWindow): + PPQ = 48.0 - if self.back.stretch: - self.expand(self.back, offset) - else: - self.move_pos = self.scenePos() + offset \ - + QPointF(self.moving_diff[0],self.moving_diff[1]) - pos = self.piano().enforce_bounds(self.move_pos) - pos_x, pos_y = pos.x(), pos.y() - pos_sx, pos_sy = self.piano().snap(pos_x, pos_y) - self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy) - if self.front.stretch: - right = self.rect().right() - offset.x() + self.expand_diff - if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \ - or right < 10: - self.expand_diff = 0 - return - self.expand(self.front, offset) - self.setPos(pos_sx, self.scenePos().y()) - else: - self.setPos(pos_sx, pos_sy) - - def expand(self, rectItem, offset): - rect = self.rect() - right = rect.right() + self.expand_diff - if rectItem == self.back: - right += offset.x() - if right > self.piano().grid_width: - right = self.piano().grid_width - elif right < 10: - right = 10 - new_x = self.piano().snap(right) - else: - right -= offset.x() - new_x = self.piano().snap(right+2.75) - if self.piano().snap_value: new_x -= 2.75 # where does this number come from?! - self.expand_diff = right - new_x - self.back.setPos(new_x - 5, 0) - rect.setRight(new_x) - self.setRect(rect) - - def updateNoteInfo(self, pos_x, pos_y): - self.note[0] = self.piano().get_note_num_from_y(pos_y) - self.note[1] = self.piano().get_note_start_from_x(pos_x) - self.note[2] = self.piano().get_note_length_from_x( - self.rect().right() - self.rect().left()) - #print("note: {}".format(self.note)) - - def mouseReleaseEvent(self, event): - QGraphicsRectItem.mouseReleaseEvent(self, event) - self.pressed = False - if event.button() == Qt.LeftButton: - self.moving_diff = (0,0) - self.expand_diff = 0 - self.back.stretch = False - self.front.stretch = False - (pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y()) - self.setPos(pos_x, pos_y) - self.updateNoteInfo(pos_x, pos_y) - - def updateVelocity(self, event): - offset = event.scenePos().x() - event.lastScenePos().x() - self.note[3] += int(offset/5) - if self.note[3] > 127: - self.note[3] = 127 - elif self.note[3] < 0: - self.note[3] = 0 - print("new vel: {}".format(self.note[3])) - self.orig_brush = QColor(self.note[3], 0, 0) - self.select_brush = QColor(self.note[3] + 100, 100, 100) - self.setBrush(self.orig_brush) - - -class PianoKeyItem(QGraphicsRectItem): - def __init__(self, width, height, parent): - QGraphicsRectItem.__init__(self, 0, 0, width, height, parent) - self.setPen(QPen(QColor(0,0,0,80))) - self.width = width - self.height = height - self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) - self.setAcceptHoverEvents(True) - self.hover_brush = QColor(200, 0, 0) - self.click_brush = QColor(255, 100, 100) - self.pressed = False - - def hoverEnterEvent(self, event): - QGraphicsRectItem.hoverEnterEvent(self, event) - self.orig_brush = self.brush() - self.setBrush(self.hover_brush) - - def hoverLeaveEvent(self, event): - if self.pressed: - self.pressed = False - self.setBrush(self.hover_brush) - QGraphicsRectItem.hoverLeaveEvent(self, event) - self.setBrush(self.orig_brush) - - #def mousePressEvent(self, event): - # self.pressed = True - # self.setBrush(self.click_brush) - - def mouseMoveEvent(self, event): - """this may eventually do something""" - pass - - def mouseReleaseEvent(self, event): - self.pressed = False - QGraphicsRectItem.mouseReleaseEvent(self, event) - self.setBrush(self.hover_brush) - -class PianoRoll(QGraphicsScene): - '''the piano roll''' - - midievent = pyqtSignal(list) - measureupdate = pyqtSignal(int) - modeupdate = pyqtSignal(str) - - def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'): - QGraphicsScene.__init__(self) - self.setBackgroundBrush(QColor(50, 50, 50)) - self.mousePos = QPointF() - - self.notes = [] - self.selected_notes = [] - self.piano_keys = [] - - self.marquee_select = False - self.insert_mode = False - self.velocity_mode = False - self.place_ghost = False - self.ghost_note = None - self.default_ghost_vel = 100 - self.ghost_vel = self.default_ghost_vel - - ## 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 - 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.setTimeSig(time_sig) - self.setMeasures(num_measures) - self.setGridDiv() - self.default_length = 1. / self.grid_div - - - # ------------------------------------------------------------------------- - # Callbacks - - def movePlayHead(self, transport_info): - # TODO: need conversion between frames and PPQ - x = 105. # works for 120bpm - total_duration = self.time_sig[0] * self.num_measures * x - pos = transport_info['frame'] / x - frac = (pos % total_duration) / total_duration - self.play_head.setPos(QPointF(frac * self.grid_width, 0)) - - def setTimeSig(self, time_sig): - try: - new_time_sig = list(map(float, time_sig.split('/'))) - if len(new_time_sig)==2: - self.time_sig = new_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() - except ValueError: - pass - - 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): - try: - 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.mousePos) - if self.insert_mode: self.makeGhostNote(pos.x(), pos.y()) - except ValueError: - pass - - 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): - try: - 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 - except ValueError: - pass - - # ------------------------------------------------------------------------- - # Event Callbacks - - def keyPressEvent(self, event): - QGraphicsScene.keyPressEvent(self, event) - if event.key() == Qt.Key_F: - if not self.insert_mode: - self.velocity_mode = False - self.insert_mode = True - self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) - self.modeupdate.emit('insert_mode') - elif self.insert_mode: - self.insert_mode = False - if self.place_ghost: self.place_ghost = False - self.removeItem(self.ghost_note) - self.ghost_note = None - self.modeupdate.emit('') - elif event.key() == Qt.Key_D: - if self.velocity_mode: - self.velocity_mode = False - self.modeupdate.emit('') - else: - if self.insert_mode: - self.removeItem(self.ghost_note) - self.ghost_note = None - self.insert_mode = False - self.place_ghost = False - self.velocity_mode = True - self.modeupdate.emit('velocity_mode') - elif event.key() == Qt.Key_A: - if all((note.isSelected() for note in self.notes)): - for note in self.notes: - note.setSelected(False) - self.selected_notes = [] - else: - for note in self.notes: - note.setSelected(True) - self.selected_notes = self.notes[:] - elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace): - self.notes = [note for note in self.notes if note not in self.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) - if not (any(key.pressed for key in self.piano_keys) - or any(note.pressed for note in self.notes)): - for note in self.selected_notes: - note.setSelected(False) - self.selected_notes = [] - - if event.button() == Qt.LeftButton: - 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) - else: - for s_note in self.notes: - if s_note.pressed and s_note in self.selected_notes: - break - elif s_note.pressed and s_note not in self.selected_notes: - for note in self.selected_notes: - note.setSelected(False) - self.selected_notes = [s_note] - break - for note in self.selected_notes: - if not self.velocity_mode: - note.mousePressEvent(event) - - def mouseMoveEvent(self, event): - QGraphicsScene.mouseMoveEvent(self, event) - self.mousePos = event.scenePos() - if not (any((key.pressed for key in self.piano_keys))): - m_pos = event.scenePos() - if self.insert_mode and self.place_ghost: #placing a note - m_width = self.ghost_rect.x() + self.ghost_rect_orig_width - if m_pos.x() > m_width: - m_new_x = self.snap(m_pos.x()) - self.ghost_rect.setRight(m_new_x) - self.ghost_note.setRect(self.ghost_rect) - #self.adjust_note_vel(event) - else: - m_pos = self.enforce_bounds(m_pos) - - if self.insert_mode: #ghostnote follows mouse around - (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y()) - self.ghost_rect.moveTo(m_new_x, m_new_y) - try: - self.ghost_note.setRect(self.ghost_rect) - except RuntimeError: - self.ghost_note = None - self.makeGhostNote(m_new_x, m_new_y) - - elif self.marquee_select: - marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton) - if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y(): - self.marquee_rect.setBottomRight(m_pos) - elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y(): - self.marquee_rect.setTopRight(m_pos) - elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y(): - self.marquee_rect.setBottomLeft(m_pos) - elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y(): - self.marquee_rect.setTopLeft(m_pos) - self.marquee.setRect(self.marquee_rect) - self.selected_notes = [] - for item in self.collidingItems(self.marquee): - if item in self.notes: - self.selected_notes.append(item) - - for note in self.notes: - if note in self.selected_notes: note.setSelected(True) - else: note.setSelected(False) - - elif self.velocity_mode: - if Qt.LeftButton == event.buttons(): - for note in self.selected_notes: - note.updateVelocity(event) - - elif not self.marquee_select: #move selected - if Qt.LeftButton == event.buttons(): - x = y = False - if any(note.back.stretch for note in self.selected_notes): - x = True - elif any(note.front.stretch for note in self.selected_notes): - y = True - for note in self.selected_notes: - note.back.stretch = x - note.front.stretch = y - note.moveEvent(event) - - def mouseReleaseEvent(self, event): - if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))): - if event.button() == Qt.LeftButton: - if self.place_ghost and self.insert_mode: - 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()) - self.drawNote(note_num, note_start, note_length, self.ghost_vel) - self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel]) - self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) - elif self.marquee_select: - self.marquee_select = False - self.removeItem(self.marquee) - elif not self.marquee_select: - for note in self.selected_notes: - old_info = note.note[:] - note.mouseReleaseEvent(event) - if self.velocity_mode: - note.setSelected(True) - if not old_info == note.note: - self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]]) - self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]]) - - # ------------------------------------------------------------------------- - # 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, self.piano) - label = QGraphicsSimpleTextItem('C8', 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, self.start_octave - self.start_octave, -1): - for j in range(self.notes_in_octave, 0, -1): - if j in black_notes: - key = PianoKeyItem(piano_keys_width/1.4, self.note_height, 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, 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, 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, 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), 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.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.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]]) - list(map(self.addItem, self.notes)) - if self.views(): - self.views()[0].setSceneRect(self.itemsBoundingRect()) - - def clearNotes(self): - self.clear() - self.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: - self.removeItem(self.ghost_note) - length = self.full_note_width * self.default_length - (start, note) = self.snap(pos_x, pos_y) - self.ghost_vel = self.default_ghost_vel - self.ghost_rect = QRectF(start, note, 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=None, note_length=None, note_velocity=None, 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) - - # ------------------------------------------------------------------------- - # 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: - 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 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): - 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) - 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 -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1) - -class PianoRollView(QGraphicsView): - def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'): - QGraphicsView.__init__(self) - 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) - -# ------------------------------------------------------------------------------------------------------------ -# External UI - -class ModeIndicator(QWidget): def __init__(self): - QWidget.__init__(self) - #self.setGeometry(0, 0, 30, 20) - self.setFixedSize(30,20) - self.mode = None - - def paintEvent(self, event): - painter = QPainter() - painter.begin(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) - painter.end() - - def changeMode(self, new_mode): - self.mode = new_mode - self.update() - - -class MainWindow(ExternalUI, QWidget): - def __init__(self): - ExternalUI.__init__(self) - QWidget.__init__(self) + QMainWindow.__init__(self) + self.ui = ui_midipattern.Ui_MidiPatternW() + self.ui.setupUi(self) + self.ui.piano = self.ui.graphicsView.piano # to be filled with note-on events, while waiting for their matching note-off self.fPendingNoteOns = [] # (channel, note, velocity, time) @@ -910,105 +74,50 @@ class MainWindow(ExternalUI, QWidget): "sigDenom": 4.0 } - self.PPQ = 48. + self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode) + self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode) + self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll) - self.initUI() - self.piano.midievent.connect(self.sendMsg) - self.piano.measureupdate.connect(self.updateMeasureBox) - self.piano.modeupdate.connect(self.modeIndicator.changeMode) + self.ui.piano.midievent.connect(self.sendMsg) + self.ui.piano.measureupdate.connect(self.updateMeasureBox) + self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode) + self.ui.piano.modeupdate.connect(self.slot_modeChanged) + + self.ui.timeSigBox.currentIndexChanged[str].connect(self.ui.piano.setTimeSig) + self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures) + self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength) + self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv) + self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX) + self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY) + + self.ui.graphicsView.setFocus() self.fIdleTimer = self.startTimer(30) self.setWindowTitle(self.fUiName) self.ready() - - def initUI(self): - self.view = PianoRollView( - time_sig = "{}/{}".format( - int(self.fTransportInfo["sigNum"]), - int(self.fTransportInfo["sigNum"])), - num_measures = 4, - quantize_val = '1/8') - - self.piano = self.view.piano - - self.timeSigLabel = QLabel('time signature') - self.timeSigLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter) - self.timeSigLabel.setMaximumWidth(100) - self.timeSigBox = QComboBox() - self.timeSigBox.setEditable(True) - self.timeSigBox.setMaximumWidth(100) - self.timeSigBox.addItems( - ('1/4', '2/4', '3/4', '4/4', '5/4', '6/4', '12/8')) - self.timeSigBox.setCurrentIndex(3) - - self.measureLabel = QLabel('measures') - self.measureLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter) - self.measureLabel.setMaximumWidth(100) - self.measureBox = QComboBox() - self.measureBox.setMaximumWidth(100) - self.measureBox.addItems(list(map(str, range(1,17)))) - self.measureBox.setCurrentIndex(3) - - self.defaultLengthLabel = QLabel('default length') - self.defaultLengthLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter) - self.defaultLengthLabel.setMaximumWidth(100) - self.defaultLengthBox = QComboBox() - self.defaultLengthBox.setEditable(True) - self.defaultLengthBox.setMaximumWidth(100) - self.defaultLengthBox.addItems(('1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1')) - self.defaultLengthBox.setCurrentIndex(4) - - self.quantizeLabel = QLabel('quantize') - self.quantizeLabel.setAlignment(Qt.AlignRight | Qt.AlignCenter) - self.quantizeLabel.setMaximumWidth(100) - self.quantizeBox = QComboBox() - self.quantizeBox.setEditable(True) - self.quantizeBox.setMaximumWidth(100) - self.quantizeBox.addItems(('0', '1/16', '1/15', '1/12', '1/9', '1/8', '1/6', '1/4', '1/3', '1/2', '1')) - self.quantizeBox.setCurrentIndex(5) - - self.hSlider = QSlider(Qt.Horizontal) - self.hSlider.setTracking(True) - #hSlider.setMaximum(1920*6*3*4) - - self.vSlider = QSlider(Qt.Vertical) - self.vSlider.setTracking(True) - self.vSlider.setInvertedAppearance(True) - self.vSlider.setMaximumHeight(500) - - self.modeIndicator = ModeIndicator() - - self.timeSigBox.currentIndexChanged[str].connect(self.piano.setTimeSig) - self.measureBox.currentIndexChanged[str].connect(self.piano.setMeasures) - self.defaultLengthBox.currentIndexChanged[str].connect(self.piano.setDefaultLength) - self.quantizeBox.currentIndexChanged[str].connect(self.piano.setGridDiv) - self.hSlider.valueChanged.connect(self.view.setZoomX) - self.vSlider.valueChanged.connect(self.view.setZoomY) - - self.hBox = QHBoxLayout() - - self.hBox.addWidget(self.modeIndicator) - self.hBox.addWidget(self.timeSigLabel) - self.hBox.addWidget(self.timeSigBox) - self.hBox.addWidget(self.measureLabel) - self.hBox.addWidget(self.measureBox) - self.hBox.addWidget(self.defaultLengthLabel) - self.hBox.addWidget(self.defaultLengthBox) - self.hBox.addWidget(self.quantizeLabel) - self.hBox.addWidget(self.quantizeBox) - self.hBox.addWidget(self.hSlider) - - self.viewBox = QHBoxLayout() - self.viewBox.addWidget(self.vSlider) - self.viewBox.addWidget(self.view) - self.layout = QVBoxLayout() - - self.layout.addLayout(self.hBox) - self.layout.addLayout(self.viewBox) - - self.setLayout(self.layout) - self.view.setFocus() + def slot_editInsertMode(self): + ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier) + self.ui.piano.keyPressEvent(ev) + + def slot_editVelocityMode(self): + ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier) + self.ui.piano.keyPressEvent(ev) + + def slot_editSelectAll(self): + ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier) + self.ui.piano.keyPressEvent(ev) + + def slot_modeChanged(self, mode): + if mode == "insert_mode": + self.ui.act_edit_insert.setChecked(True) + self.ui.act_edit_velocity.setChecked(False) + elif mode == "velocity_mode": + self.ui.act_edit_insert.setChecked(False) + self.ui.act_edit_velocity.setChecked(True) + else: + self.ui.act_edit_insert.setChecked(False) + self.ui.act_edit_velocity.setChecked(False) # ------------------------------------------------------------------- # DSP Callbacks @@ -1049,11 +158,11 @@ class MainWindow(ExternalUI, QWidget): def timerEvent(self, event): if event.timerId() == self.fIdleTimer: self.idleExternalUI() - QGraphicsView.timerEvent(self, event) + QMainWindow.timerEvent(self, event) def closeEvent(self, event): self.closeExternalUI() - QGraphicsView.closeEvent(self, event) + QMainWindow.closeEvent(self, event) # there might be other qt windows open which will block the UI from quitting app.quit() @@ -1080,7 +189,6 @@ class MainWindow(ExternalUI, QWidget): self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel]) self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel]) - def msgCallback(self, msg): #try: self.msgCallback2(msg) @@ -1092,7 +200,7 @@ class MainWindow(ExternalUI, QWidget): if msg == "midi-clear-all": # clear all notes - self.piano.clearNotes() + self.ui.piano.clearNotes() elif msg == "midievent-add": # adds single midi event @@ -1127,8 +235,7 @@ class MainWindow(ExternalUI, QWidget): } if old_frame != frame: - self.piano.movePlayHead(self.fTransportInfo) - + self.ui.piano.movePlayHead(self.fTransportInfo) elif msg == "show": self.uiShow() @@ -1188,7 +295,7 @@ class MainWindow(ExternalUI, QWidget): #print("{} {} {} {}\n".format(note, time_, time-time_, velo_)) start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"] length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"] - self.piano.drawNote(note, start, length, velo_) + self.ui.piano.drawNote(note, start, length, velo_) # remove from list self.fPendingNoteOns.remove(noteOnMsg) @@ -1200,8 +307,8 @@ if __name__ == '__main__': pathBinaries, pathResources = getPaths() gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION)) - gCarla.utils.set_process_name("MidiSequencer") + gCarla.utils.set_process_name("MidiPattern") - app = CarlaApplication("MidiSequencer") - gui = MainWindow() + app = CarlaApplication("MidiPattern") + gui = MidiPatternW() app.exit_exec() diff --git a/source/widgets/pianoroll.py b/source/widgets/pianoroll.py new file mode 100755 index 000000000..f59fe1064 --- /dev/null +++ b/source/widgets/pianoroll.py @@ -0,0 +1,887 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# A piano roll viewer/editor +# Copyright (C) 2012-2015 Filipe Coelho +# 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 (Config) + +from carla_config import * + +# ------------------------------------------------------------------------------------------------------------ +# Imports (Global) + +if config_UseQt5: + from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal + from PyQt5.QtGui import QColor, QFont, QPen, QPainter + from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem + from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView + from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle +else: + from PyQt4.QtCore import Qt, QRectF, QPointF, pyqtSignal + from PyQt4.QtGui import QColor, QFont, QPen, QPainter + from PyQt4.QtGui import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem + from PyQt4.QtGui import QGraphicsScene, QGraphicsView + from PyQt4.QtGui import QWidget, QLabel, QComboBox, QSlider, QHBoxLayout, QVBoxLayout, QStyle + +# ------------------------------------------------------------------------------------------------------------ +# 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.setFlag(QGraphicsItem.ItemSendsGeometryChanges) + self.setAcceptHoverEvents(True) + + clearpen = QPen(QColor(0,0,0,0)) + self.setPen(clearpen) + + self.orig_brush = QColor(0, 0, 0, 0) + self.hover_brush = QColor(200, 200, 200) + self.stretch = False + + def mousePressEvent(self, event): + QGraphicsRectItem.mousePressEvent(self, event) + self.stretch = True + + def hoverEnterEvent(self, event): + QGraphicsRectItem.hoverEnterEvent(self, event) + if self.parent.isSelected(): + self.parent.setBrush(self.parent.select_brush) + else: + self.parent.setBrush(self.parent.orig_brush) + self.setBrush(self.hover_brush) + + def hoverLeaveEvent(self, event): + QGraphicsRectItem.hoverLeaveEvent(self, event) + if self.parent.isSelected(): + self.parent.setBrush(self.parent.select_brush) + elif self.parent.hovering: + self.parent.setBrush(self.parent.hover_brush) + else: + self.parent.setBrush(self.parent.orig_brush) + 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.setFlag(QGraphicsItem.ItemIsMovable) + self.setFlag(QGraphicsItem.ItemIsSelectable) + self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) + self.setAcceptHoverEvents(True) + + clearpen = QPen(QColor(0,0,0,0)) + self.setPen(clearpen) + self.orig_brush = QColor(note_info[3], 0, 0) + self.hover_brush = QColor(note_info[3] + 100, 200, 100) + self.select_brush = QColor(note_info[3] + 100, 100, 100) + self.setBrush(self.orig_brush) + + 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 + + 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 + QGraphicsRectItem.paint(self, painter, paint_option, widget) + + def setSelected(self, boolean): + QGraphicsRectItem.setSelected(self, boolean) + if boolean: self.setBrush(self.select_brush) + else: self.setBrush(self.orig_brush) + + def hoverEnterEvent(self, event): + self.hovering = True + QGraphicsRectItem.hoverEnterEvent(self, event) + if not self.isSelected(): + self.setBrush(self.hover_brush) + + def hoverLeaveEvent(self, event): + self.hovering = False + QGraphicsRectItem.hoverLeaveEvent(self, event) + if not self.isSelected(): + self.setBrush(self.orig_brush) + elif self.isSelected(): + self.setBrush(self.select_brush) + + def mousePressEvent(self, event): + QGraphicsRectItem.mousePressEvent(self, event) + self.setSelected(True) + self.pressed = True + + def mouseMoveEvent(self, event): + pass + + def moveEvent(self, event): + offset = event.scenePos() - event.lastScenePos() + + if self.back.stretch: + self.expand(self.back, offset) + else: + self.move_pos = self.scenePos() + offset \ + + QPointF(self.moving_diff[0],self.moving_diff[1]) + pos = self.piano().enforce_bounds(self.move_pos) + pos_x, pos_y = pos.x(), pos.y() + pos_sx, pos_sy = self.piano().snap(pos_x, pos_y) + self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy) + if self.front.stretch: + right = self.rect().right() - offset.x() + self.expand_diff + if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \ + or right < 10: + self.expand_diff = 0 + return + self.expand(self.front, offset) + self.setPos(pos_sx, self.scenePos().y()) + else: + self.setPos(pos_sx, pos_sy) + + def expand(self, rectItem, offset): + rect = self.rect() + right = rect.right() + self.expand_diff + if rectItem == self.back: + right += offset.x() + if right > self.piano().grid_width: + right = self.piano().grid_width + elif right < 10: + right = 10 + new_x = self.piano().snap(right) + else: + right -= offset.x() + new_x = self.piano().snap(right+2.75) + if self.piano().snap_value: new_x -= 2.75 # where does this number come from?! + self.expand_diff = right - new_x + self.back.setPos(new_x - 5, 0) + rect.setRight(new_x) + self.setRect(rect) + + def updateNoteInfo(self, pos_x, pos_y): + self.note[0] = self.piano().get_note_num_from_y(pos_y) + self.note[1] = self.piano().get_note_start_from_x(pos_x) + self.note[2] = self.piano().get_note_length_from_x( + self.rect().right() - self.rect().left()) + #print("note: {}".format(self.note)) + + def mouseReleaseEvent(self, event): + QGraphicsRectItem.mouseReleaseEvent(self, event) + self.pressed = False + if event.button() == Qt.LeftButton: + self.moving_diff = (0,0) + self.expand_diff = 0 + self.back.stretch = False + self.front.stretch = False + (pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y()) + self.setPos(pos_x, pos_y) + self.updateNoteInfo(pos_x, pos_y) + + def updateVelocity(self, event): + offset = event.scenePos().x() - event.lastScenePos().x() + self.note[3] += int(offset/5) + if self.note[3] > 127: + self.note[3] = 127 + elif self.note[3] < 0: + self.note[3] = 0 + print("new vel: {}".format(self.note[3])) + self.orig_brush = QColor(self.note[3], 0, 0) + self.select_brush = QColor(self.note[3] + 100, 100, 100) + self.setBrush(self.orig_brush) + + +class PianoKeyItem(QGraphicsRectItem): + def __init__(self, width, height, parent): + QGraphicsRectItem.__init__(self, 0, 0, width, height, parent) + self.setPen(QPen(QColor(0,0,0,80))) + self.width = width + self.height = height + self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) + self.setAcceptHoverEvents(True) + self.hover_brush = QColor(200, 0, 0) + self.click_brush = QColor(255, 100, 100) + self.pressed = False + + def hoverEnterEvent(self, event): + QGraphicsRectItem.hoverEnterEvent(self, event) + self.orig_brush = self.brush() + self.setBrush(self.hover_brush) + + def hoverLeaveEvent(self, event): + if self.pressed: + self.pressed = False + self.setBrush(self.hover_brush) + QGraphicsRectItem.hoverLeaveEvent(self, event) + self.setBrush(self.orig_brush) + + #def mousePressEvent(self, event): + # self.pressed = True + # self.setBrush(self.click_brush) + + def mouseMoveEvent(self, event): + """this may eventually do something""" + pass + + def mouseReleaseEvent(self, event): + self.pressed = False + QGraphicsRectItem.mouseReleaseEvent(self, event) + self.setBrush(self.hover_brush) + +class PianoRoll(QGraphicsScene): + '''the piano roll''' + + midievent = pyqtSignal(list) + measureupdate = pyqtSignal(int) + modeupdate = pyqtSignal(str) + + def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'): + QGraphicsScene.__init__(self) + self.setBackgroundBrush(QColor(50, 50, 50)) + self.mousePos = QPointF() + + self.notes = [] + self.selected_notes = [] + self.piano_keys = [] + + self.marquee_select = False + self.insert_mode = False + self.velocity_mode = False + self.place_ghost = False + self.ghost_note = None + self.default_ghost_vel = 100 + self.ghost_vel = self.default_ghost_vel + + ## 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 + 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.setTimeSig(time_sig) + self.setMeasures(num_measures) + self.setGridDiv() + self.default_length = 1. / self.grid_div + + + # ------------------------------------------------------------------------- + # Callbacks + + def movePlayHead(self, transport_info): + # TODO: need conversion between frames and PPQ + x = 105. # works for 120bpm + total_duration = self.time_sig[0] * self.num_measures * x + pos = transport_info['frame'] / x + frac = (pos % total_duration) / total_duration + self.play_head.setPos(QPointF(frac * self.grid_width, 0)) + + def setTimeSig(self, time_sig): + try: + new_time_sig = list(map(float, time_sig.split('/'))) + if len(new_time_sig)==2: + self.time_sig = new_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() + except ValueError: + pass + + 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): + try: + 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.mousePos) + if self.insert_mode: self.makeGhostNote(pos.x(), pos.y()) + except ValueError: + pass + + 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): + try: + 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 + except ValueError: + pass + + # ------------------------------------------------------------------------- + # Event Callbacks + + def keyPressEvent(self, event): + QGraphicsScene.keyPressEvent(self, event) + if event.key() == Qt.Key_F: + if not self.insert_mode: + self.velocity_mode = False + self.insert_mode = True + self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) + self.modeupdate.emit('insert_mode') + elif self.insert_mode: + self.insert_mode = False + if self.place_ghost: self.place_ghost = False + self.removeItem(self.ghost_note) + self.ghost_note = None + self.modeupdate.emit('') + elif event.key() == Qt.Key_D: + if self.velocity_mode: + self.velocity_mode = False + self.modeupdate.emit('') + else: + if self.insert_mode: + self.removeItem(self.ghost_note) + self.ghost_note = None + self.insert_mode = False + self.place_ghost = False + self.velocity_mode = True + self.modeupdate.emit('velocity_mode') + elif event.key() == Qt.Key_A: + if all((note.isSelected() for note in self.notes)): + for note in self.notes: + note.setSelected(False) + self.selected_notes = [] + else: + for note in self.notes: + note.setSelected(True) + self.selected_notes = self.notes[:] + elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace): + self.notes = [note for note in self.notes if note not in self.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) + if not (any(key.pressed for key in self.piano_keys) + or any(note.pressed for note in self.notes)): + for note in self.selected_notes: + note.setSelected(False) + self.selected_notes = [] + + if event.button() == Qt.LeftButton: + 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) + else: + for s_note in self.notes: + if s_note.pressed and s_note in self.selected_notes: + break + elif s_note.pressed and s_note not in self.selected_notes: + for note in self.selected_notes: + note.setSelected(False) + self.selected_notes = [s_note] + break + for note in self.selected_notes: + if not self.velocity_mode: + note.mousePressEvent(event) + + def mouseMoveEvent(self, event): + QGraphicsScene.mouseMoveEvent(self, event) + self.mousePos = event.scenePos() + if not (any((key.pressed for key in self.piano_keys))): + m_pos = event.scenePos() + if self.insert_mode and self.place_ghost: #placing a note + m_width = self.ghost_rect.x() + self.ghost_rect_orig_width + if m_pos.x() > m_width: + m_new_x = self.snap(m_pos.x()) + self.ghost_rect.setRight(m_new_x) + self.ghost_note.setRect(self.ghost_rect) + #self.adjust_note_vel(event) + else: + m_pos = self.enforce_bounds(m_pos) + + if self.insert_mode: #ghostnote follows mouse around + (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y()) + self.ghost_rect.moveTo(m_new_x, m_new_y) + try: + self.ghost_note.setRect(self.ghost_rect) + except RuntimeError: + self.ghost_note = None + self.makeGhostNote(m_new_x, m_new_y) + + elif self.marquee_select: + marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton) + if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y(): + self.marquee_rect.setBottomRight(m_pos) + elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y(): + self.marquee_rect.setTopRight(m_pos) + elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y(): + self.marquee_rect.setBottomLeft(m_pos) + elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y(): + self.marquee_rect.setTopLeft(m_pos) + self.marquee.setRect(self.marquee_rect) + self.selected_notes = [] + for item in self.collidingItems(self.marquee): + if item in self.notes: + self.selected_notes.append(item) + + for note in self.notes: + if note in self.selected_notes: note.setSelected(True) + else: note.setSelected(False) + + elif self.velocity_mode: + if Qt.LeftButton == event.buttons(): + for note in self.selected_notes: + note.updateVelocity(event) + + elif not self.marquee_select: #move selected + if Qt.LeftButton == event.buttons(): + x = y = False + if any(note.back.stretch for note in self.selected_notes): + x = True + elif any(note.front.stretch for note in self.selected_notes): + y = True + for note in self.selected_notes: + note.back.stretch = x + note.front.stretch = y + note.moveEvent(event) + + def mouseReleaseEvent(self, event): + if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))): + if event.button() == Qt.LeftButton: + if self.place_ghost and self.insert_mode: + 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()) + self.drawNote(note_num, note_start, note_length, self.ghost_vel) + self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel]) + self.makeGhostNote(self.mousePos.x(), self.mousePos.y()) + elif self.marquee_select: + self.marquee_select = False + self.removeItem(self.marquee) + elif not self.marquee_select: + for note in self.selected_notes: + old_info = note.note[:] + note.mouseReleaseEvent(event) + if self.velocity_mode: + note.setSelected(True) + if not old_info == note.note: + self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]]) + self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]]) + + # ------------------------------------------------------------------------- + # 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, self.piano) + label = QGraphicsSimpleTextItem('C8', 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, self.start_octave - self.start_octave, -1): + for j in range(self.notes_in_octave, 0, -1): + if j in black_notes: + key = PianoKeyItem(piano_keys_width/1.4, self.note_height, 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, 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, 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, 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), 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.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.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]]) + list(map(self.addItem, self.notes)) + if self.views(): + self.views()[0].setSceneRect(self.itemsBoundingRect()) + + def clearNotes(self): + self.clear() + self.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: + self.removeItem(self.ghost_note) + length = self.full_note_width * self.default_length + (start, note) = self.snap(pos_x, pos_y) + self.ghost_vel = self.default_ghost_vel + self.ghost_rect = QRectF(start, note, 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=None, note_length=None, note_velocity=None, 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) + + # ------------------------------------------------------------------------- + # 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: + 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 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): + 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) + 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 -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1) + +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.setFixedSize(30,20) + self.mode = None + + def paintEvent(self, event): + painter = QPainter(self) + event.accept() + + painter.begin(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) + + # FIXME + painter.end() + + def changeMode(self, new_mode): + self.mode = new_mode + self.update() + +# ------------------------------------------------------------------------------------------------------------