Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pianoroll.py 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # A piano roll viewer/editor
  4. # Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
  5. # Copyright (C) 2014-2015 Perry Nguyen
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License as
  9. # published by the Free Software Foundation; either version 2 of
  10. # the License, or any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  18. # ------------------------------------------------------------------------------------------------------------
  19. # Imports (Global)
  20. from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
  21. from PyQt5.QtGui import QColor, QCursor, QFont, QPen, QPainter, QTransform
  22. from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  23. from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
  24. from PyQt5.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
  25. # ------------------------------------------------------------------------------------------------------------
  26. # Imports (Custom)
  27. from carla_shared import *
  28. # ------------------------------------------------------------------------------------------------------------
  29. # MIDI definitions, copied from CarlaMIDI.h
  30. MAX_MIDI_CHANNELS = 16
  31. MAX_MIDI_NOTE = 128
  32. MAX_MIDI_VALUE = 128
  33. MAX_MIDI_CONTROL = 120 # 0x77
  34. MIDI_STATUS_BIT = 0xF0
  35. MIDI_CHANNEL_BIT = 0x0F
  36. # MIDI Messages List
  37. MIDI_STATUS_NOTE_OFF = 0x80 # note (0-127), velocity (0-127)
  38. MIDI_STATUS_NOTE_ON = 0x90 # note (0-127), velocity (0-127)
  39. MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
  40. MIDI_STATUS_CONTROL_CHANGE = 0xB0 # see 'Control Change Messages List'
  41. MIDI_STATUS_PROGRAM_CHANGE = 0xC0 # program (0-127), none
  42. MIDI_STATUS_CHANNEL_PRESSURE = 0xD0 # pressure (0-127), none
  43. MIDI_STATUS_PITCH_WHEEL_CONTROL = 0xE0 # LSB (0-127), MSB (0-127)
  44. # MIDI Message type
  45. def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status < MIDI_STATUS_BIT
  46. def MIDI_IS_SYSTEM_MESSAGE(status): return status >= MIDI_STATUS_BIT and status <= 0xFF
  47. def MIDI_IS_OSC_MESSAGE(status): return status == '/' or status == '#'
  48. # MIDI Channel message type
  49. def MIDI_IS_STATUS_NOTE_OFF(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
  50. def MIDI_IS_STATUS_NOTE_ON(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
  51. def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
  52. def MIDI_IS_STATUS_CONTROL_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
  53. def MIDI_IS_STATUS_PROGRAM_CHANGE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
  54. def MIDI_IS_STATUS_CHANNEL_PRESSURE(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
  55. def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL
  56. # MIDI Utils
  57. def MIDI_GET_STATUS_FROM_DATA(data): return data[0] & MIDI_STATUS_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
  58. def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0
  59. # ---------------------------------------------------------------------------------------------------------------------
  60. # Graphics Items
  61. class NoteExpander(QGraphicsRectItem):
  62. def __init__(self, length, height, parent):
  63. QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
  64. self.parent = parent
  65. self.orig_brush = QColor(0, 0, 0, 0)
  66. self.hover_brush = QColor(200, 200, 200)
  67. self.stretch = False
  68. self.setAcceptHoverEvents(True)
  69. self.setFlag(QGraphicsItem.ItemIsSelectable)
  70. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  71. self.setPen(QPen(QColor(0,0,0,0)))
  72. def paint(self, painter, option, widget=None):
  73. paint_option = option
  74. paint_option.state &= ~QStyle.State_Selected
  75. QGraphicsRectItem.paint(self, painter, paint_option, widget)
  76. def mousePressEvent(self, event):
  77. QGraphicsRectItem.mousePressEvent(self, event)
  78. self.stretch = True
  79. def mouseReleaseEvent(self, event):
  80. QGraphicsRectItem.mouseReleaseEvent(self, event)
  81. self.stretch = False
  82. def hoverEnterEvent(self, event):
  83. QGraphicsRectItem.hoverEnterEvent(self, event)
  84. self.setCursor(QCursor(Qt.SizeHorCursor))
  85. self.setBrush(self.hover_brush)
  86. def hoverLeaveEvent(self, event):
  87. QGraphicsRectItem.hoverLeaveEvent(self, event)
  88. self.unsetCursor()
  89. self.setBrush(self.orig_brush)
  90. # ---------------------------------------------------------------------------------------------------------------------
  91. class NoteItem(QGraphicsRectItem):
  92. '''a note on the pianoroll sequencer'''
  93. def __init__(self, height, length, note_info):
  94. QGraphicsRectItem.__init__(self, 0, 0, length, height)
  95. self.orig_brush = QColor(note_info[3], 0, 0)
  96. self.hover_brush = QColor(note_info[3] + 98, 200, 100)
  97. self.select_brush = QColor(note_info[3] + 98, 100, 100)
  98. self.note = note_info
  99. self.length = length
  100. self.piano = self.scene
  101. self.pressed = False
  102. self.hovering = False
  103. self.moving_diff = (0,0)
  104. self.expand_diff = 0
  105. self.setAcceptHoverEvents(True)
  106. self.setFlag(QGraphicsItem.ItemIsMovable)
  107. self.setFlag(QGraphicsItem.ItemIsSelectable)
  108. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  109. self.setPen(QPen(QColor(0,0,0,0)))
  110. self.setBrush(self.orig_brush)
  111. l = 5
  112. self.front = NoteExpander(l, height, self)
  113. self.back = NoteExpander(l, height, self)
  114. self.back.setPos(length - l, 0)
  115. def paint(self, painter, option, widget=None):
  116. paint_option = option
  117. paint_option.state &= ~QStyle.State_Selected
  118. if self.isSelected():
  119. self.setBrush(self.select_brush)
  120. elif self.hovering:
  121. self.setBrush(self.hover_brush)
  122. else:
  123. self.setBrush(self.orig_brush)
  124. QGraphicsRectItem.paint(self, painter, paint_option, widget)
  125. def hoverEnterEvent(self, event):
  126. QGraphicsRectItem.hoverEnterEvent(self, event)
  127. self.hovering = True
  128. self.update()
  129. self.setCursor(QCursor(Qt.OpenHandCursor))
  130. def hoverLeaveEvent(self, event):
  131. QGraphicsRectItem.hoverLeaveEvent(self, event)
  132. self.hovering = False
  133. self.unsetCursor()
  134. self.update()
  135. def mousePressEvent(self, event):
  136. QGraphicsRectItem.mousePressEvent(self, event)
  137. self.pressed = True
  138. self.moving_diff = (0,0)
  139. self.expand_diff = 0
  140. self.setCursor(QCursor(Qt.ClosedHandCursor))
  141. self.setSelected(True)
  142. def mouseMoveEvent(self, event):
  143. event.ignore()
  144. def mouseReleaseEvent(self, event):
  145. QGraphicsRectItem.mouseReleaseEvent(self, event)
  146. self.pressed = False
  147. self.moving_diff = (0,0)
  148. self.expand_diff = 0
  149. self.setCursor(QCursor(Qt.OpenHandCursor))
  150. def moveEvent(self, event):
  151. offset = event.scenePos() - event.lastScenePos()
  152. if self.back.stretch:
  153. self.expand(self.back, offset)
  154. self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
  155. return
  156. if self.front.stretch:
  157. self.expand(self.front, offset)
  158. self.updateNoteInfo(self.scenePos().x(), self.scenePos().y())
  159. return
  160. piano = self.piano()
  161. pos = self.scenePos() + offset + QPointF(self.moving_diff[0],self.moving_diff[1])
  162. pos = piano.enforce_bounds(pos)
  163. pos_x = pos.x()
  164. pos_y = pos.y()
  165. width = self.rect().width()
  166. if pos_x + width > piano.grid_width + piano.piano_width:
  167. pos_x = piano.grid_width + piano.piano_width - width
  168. pos_sx, pos_sy = piano.snap(pos_x, pos_y)
  169. if pos_sx + width > piano.grid_width + piano.piano_width:
  170. self.moving_diff = (0,0)
  171. self.expand_diff = 0
  172. return
  173. self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
  174. self.setPos(pos_sx, pos_sy)
  175. self.updateNoteInfo(pos_sx, pos_sy)
  176. def expand(self, rectItem, offset):
  177. rect = self.rect()
  178. piano = self.piano()
  179. width = rect.right() + self.expand_diff
  180. if rectItem == self.back:
  181. width += offset.x()
  182. max_x = piano.grid_width + piano.piano_width
  183. if width + self.scenePos().x() >= max_x:
  184. width = max_x - self.scenePos().x() - 1
  185. elif piano.snap_value and width < piano.snap_value:
  186. width = piano.snap_value
  187. elif width < 10:
  188. width = 10
  189. new_w = piano.snap(width) - 2.75
  190. if new_w + self.scenePos().x() >= max_x:
  191. self.moving_diff = (0,0)
  192. self.expand_diff = 0
  193. return
  194. else:
  195. width -= offset.x()
  196. new_w = piano.snap(width+2.75) - 2.75
  197. if new_w <= 0:
  198. new_w = piano.snap_value
  199. self.moving_diff = (0,0)
  200. self.expand_diff = 0
  201. return
  202. diff = rect.right() - new_w
  203. if diff: # >= piano.snap_value:
  204. new_x = self.scenePos().x() + diff
  205. if new_x < piano.piano_width:
  206. new_x = piano.piano_width
  207. self.moving_diff = (0,0)
  208. self.expand_diff = 0
  209. return
  210. print(new_x, new_w, diff)
  211. self.setX(new_x)
  212. self.expand_diff = width - new_w
  213. self.back.setPos(new_w - 5, 0)
  214. rect.setRight(new_w)
  215. self.setRect(rect)
  216. def updateNoteInfo(self, pos_x, pos_y):
  217. note_info = (self.piano().get_note_num_from_y(pos_y),
  218. self.piano().get_note_start_from_x(pos_x),
  219. self.piano().get_note_length_from_x(self.rect().width()),
  220. self.note[3])
  221. if self.note != note_info:
  222. self.piano().move_note(self.note, note_info)
  223. self.note = note_info
  224. def updateVelocity(self, event):
  225. offset = event.scenePos().x() - event.lastScenePos().x()
  226. offset = int(offset/5)
  227. note_info = self.note[:]
  228. note_info[3] += offset
  229. if note_info[3] > 127:
  230. note_info[3] = 127
  231. elif note_info[3] < 0:
  232. note_info[3] = 0
  233. if self.note != note_info:
  234. self.orig_brush = QColor(note_info[3], 0, 0)
  235. self.hover_brush = QColor(note_info[3] + 98, 200, 100)
  236. self.select_brush = QColor(note_info[3] + 98, 100, 100)
  237. self.update()
  238. self.piano().move_note(self.note, note_info)
  239. self.note = note_info
  240. # ---------------------------------------------------------------------------------------------------------------------
  241. class PianoKeyItem(QGraphicsRectItem):
  242. def __init__(self, width, height, note, parent):
  243. QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
  244. self.width = width
  245. self.height = height
  246. self.note = note
  247. self.piano = self.scene
  248. self.hovered = False
  249. self.pressed = False
  250. self.click_brush = QColor(255, 100, 100)
  251. self.hover_brush = QColor(200, 0, 0)
  252. self.orig_brush = None
  253. self.setAcceptHoverEvents(True)
  254. self.setFlag(QGraphicsItem.ItemIsSelectable)
  255. self.setPen(QPen(QColor(0,0,0,80)))
  256. def paint(self, painter, option, widget=None):
  257. paint_option = option
  258. paint_option.state &= ~QStyle.State_Selected
  259. QGraphicsRectItem.paint(self, painter, paint_option, widget)
  260. def hoverEnterEvent(self, event):
  261. QGraphicsRectItem.hoverEnterEvent(self, event)
  262. self.hovered = True
  263. self.orig_brush = self.brush()
  264. self.setBrush(self.hover_brush)
  265. def hoverLeaveEvent(self, event):
  266. QGraphicsRectItem.hoverLeaveEvent(self, event)
  267. self.hovered = False
  268. self.setBrush(self.click_brush if self.pressed else self.orig_brush)
  269. def mousePressEvent(self, event):
  270. QGraphicsRectItem.mousePressEvent(self, event)
  271. self.pressed = True
  272. self.setBrush(self.click_brush)
  273. self.piano().noteclicked.emit(self.note, True)
  274. def mouseReleaseEvent(self, event):
  275. QGraphicsRectItem.mouseReleaseEvent(self, event)
  276. self.pressed = False
  277. self.setBrush(self.hover_brush if self.hovered else self.orig_brush)
  278. self.piano().noteclicked.emit(self.note, False)
  279. # ---------------------------------------------------------------------------------------------------------------------
  280. class PianoRoll(QGraphicsScene):
  281. '''the piano roll'''
  282. noteclicked = pyqtSignal(int,bool)
  283. midievent = pyqtSignal(list)
  284. measureupdate = pyqtSignal(int)
  285. modeupdate = pyqtSignal(str)
  286. default_ghost_vel = 100
  287. def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  288. QGraphicsScene.__init__(self)
  289. self.setBackgroundBrush(QColor(50, 50, 50))
  290. self.notes = []
  291. self.removed_notes = []
  292. self.selected_notes = []
  293. self.piano_keys = []
  294. self.marquee_select = False
  295. self.marquee_rect = None
  296. self.marquee = None
  297. self.ghost_note = None
  298. self.ghost_rect = None
  299. self.ghost_rect_orig_width = None
  300. self.ghost_vel = self.default_ghost_vel
  301. self.ignore_mouse_events = False
  302. self.insert_mode = False
  303. self.velocity_mode = False
  304. self.place_ghost = False
  305. self.last_mouse_pos = QPointF()
  306. ## dimensions
  307. self.padding = 2
  308. ## piano dimensions
  309. self.note_height = 10
  310. self.start_octave = -2
  311. self.end_octave = 8
  312. self.notes_in_octave = 12
  313. self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1
  314. self.piano_height = self.note_height * self.total_notes
  315. self.octave_height = self.notes_in_octave * self.note_height
  316. self.piano_width = 34
  317. ## height
  318. self.header_height = 20
  319. self.total_height = self.piano_height - self.note_height + self.header_height
  320. #not sure why note_height is subtracted
  321. ## width
  322. self.full_note_width = 250 # i.e. a 4/4 note
  323. self.snap_value = None
  324. self.quantize_val = quantize_val
  325. ### dummy vars that will be changed
  326. self.time_sig = (0,0)
  327. self.measure_width = 0
  328. self.num_measures = 0
  329. self.max_note_length = 0
  330. self.grid_width = 0
  331. self.value_width = 0
  332. self.grid_div = 0
  333. self.piano = None
  334. self.header = None
  335. self.play_head = None
  336. self.setGridDiv()
  337. self.default_length = 1. / self.grid_div
  338. # -------------------------------------------------------------------------
  339. # Callbacks
  340. def movePlayHead(self, transportInfo):
  341. ticksPerBeat = transportInfo['ticksPerBeat']
  342. max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures
  343. cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick']
  344. frac = (cur_tick % max_ticks) / max_ticks
  345. self.play_head.setPos(QPointF(frac * self.grid_width, 0))
  346. def setTimeSig(self, time_sig):
  347. self.time_sig = time_sig
  348. self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
  349. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  350. self.grid_width = self.measure_width * self.num_measures
  351. self.setGridDiv()
  352. def setMeasures(self, measures):
  353. #try:
  354. self.num_measures = float(measures)
  355. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  356. self.grid_width = self.measure_width * self.num_measures
  357. self.refreshScene()
  358. #except:
  359. #pass
  360. def setDefaultLength(self, length):
  361. v = list(map(float, length.split('/')))
  362. if len(v) < 3:
  363. self.default_length = v[0] if len(v) == 1 else v[0] / v[1]
  364. pos = self.enforce_bounds(self.last_mouse_pos)
  365. if self.insert_mode:
  366. self.makeGhostNote(pos.x(), pos.y())
  367. def setGridDiv(self, div=None):
  368. if not div: div = self.quantize_val
  369. try:
  370. val = list(map(int, div.split('/')))
  371. if len(val) < 3:
  372. self.quantize_val = div
  373. self.grid_div = val[0] if len(val)==1 else val[1]
  374. self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
  375. self.setQuantize(div)
  376. self.refreshScene()
  377. except ValueError:
  378. pass
  379. def setQuantize(self, value):
  380. val = list(map(float, value.split('/')))
  381. if len(val) == 1:
  382. self.quantize(val[0])
  383. self.quantize_val = value
  384. elif len(val) == 2:
  385. self.quantize(val[0] / val[1])
  386. self.quantize_val = value
  387. # -------------------------------------------------------------------------
  388. # Event Callbacks
  389. def keyPressEvent(self, event):
  390. QGraphicsScene.keyPressEvent(self, event)
  391. if event.key() == Qt.Key_Escape:
  392. QApplication.instance().closeAllWindows()
  393. return
  394. if event.key() == Qt.Key_F:
  395. if not self.insert_mode:
  396. # turn off velocity mode
  397. self.velocity_mode = False
  398. # enable insert mode
  399. self.insert_mode = True
  400. self.place_ghost = False
  401. self.makeGhostNote(self.last_mouse_pos.x(), self.last_mouse_pos.y())
  402. self.modeupdate.emit('insert_mode')
  403. else:
  404. # turn off insert mode
  405. self.insert_mode = False
  406. self.place_ghost = False
  407. if self.ghost_note is not None:
  408. self.removeItem(self.ghost_note)
  409. self.ghost_note = None
  410. self.modeupdate.emit('')
  411. elif event.key() == Qt.Key_D:
  412. if not self.velocity_mode:
  413. # turn off insert mode
  414. self.insert_mode = False
  415. self.place_ghost = False
  416. if self.ghost_note is not None:
  417. self.removeItem(self.ghost_note)
  418. self.ghost_note = None
  419. # enable velocity mode
  420. self.velocity_mode = True
  421. self.modeupdate.emit('velocity_mode')
  422. else:
  423. # turn off velocity mode
  424. self.velocity_mode = False
  425. self.modeupdate.emit('')
  426. elif event.key() == Qt.Key_A:
  427. for note in self.notes:
  428. if not note.isSelected():
  429. has_unselected = True
  430. break
  431. else:
  432. has_unselected = False
  433. # select all notes
  434. if has_unselected:
  435. for note in self.notes:
  436. note.setSelected(True)
  437. self.selected_notes = self.notes[:]
  438. # unselect all
  439. else:
  440. for note in self.notes:
  441. note.setSelected(False)
  442. self.selected_notes = []
  443. elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
  444. # remove selected notes from our notes list
  445. self.notes = [note for note in self.notes if note not in self.selected_notes]
  446. # delete the selected notes
  447. for note in self.selected_notes:
  448. self.removeItem(note)
  449. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  450. del note
  451. self.selected_notes = []
  452. def mousePressEvent(self, event):
  453. QGraphicsScene.mousePressEvent(self, event)
  454. # mouse click on left-side piano area
  455. if self.piano.contains(event.scenePos()):
  456. self.ignore_mouse_events = True
  457. return
  458. clicked_notes = []
  459. for note in self.notes:
  460. if note.pressed or note.back.stretch or note.front.stretch:
  461. clicked_notes.append(note)
  462. # mouse click on existing notes
  463. if clicked_notes:
  464. keep_selection = all(note in self.selected_notes for note in clicked_notes)
  465. if keep_selection:
  466. for note in self.selected_notes:
  467. note.setSelected(True)
  468. return
  469. for note in self.selected_notes:
  470. if note not in clicked_notes:
  471. note.setSelected(False)
  472. for note in clicked_notes:
  473. if note not in self.selected_notes:
  474. note.setSelected(True)
  475. self.selected_notes = clicked_notes
  476. return
  477. # mouse click on empty area (no note selected)
  478. for note in self.selected_notes:
  479. note.setSelected(False)
  480. self.selected_notes = []
  481. if event.button() != Qt.LeftButton:
  482. return
  483. if self.insert_mode:
  484. self.place_ghost = True
  485. else:
  486. self.marquee_select = True
  487. self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
  488. self.marquee = QGraphicsRectItem(self.marquee_rect)
  489. self.marquee.setBrush(QColor(255, 255, 255, 100))
  490. self.addItem(self.marquee)
  491. def mouseMoveEvent(self, event):
  492. QGraphicsScene.mouseMoveEvent(self, event)
  493. self.last_mouse_pos = event.scenePos()
  494. if self.ignore_mouse_events:
  495. return
  496. pos = self.enforce_bounds(self.last_mouse_pos)
  497. if self.insert_mode:
  498. if self.ghost_note is None:
  499. self.makeGhostNote(pos.x(), pos.y())
  500. max_x = self.grid_width + self.piano_width
  501. # placing note, only width needs updating
  502. if self.place_ghost:
  503. pos_x = pos.x()
  504. min_x = self.ghost_rect.x() + self.ghost_rect_orig_width
  505. if pos_x < min_x:
  506. pos_x = min_x
  507. new_x = self.snap(pos_x)
  508. self.ghost_rect.setRight(new_x)
  509. self.ghost_note.setRect(self.ghost_rect)
  510. #self.adjust_note_vel(event)
  511. # ghostnote following mouse around
  512. else:
  513. pos_x = pos.x()
  514. if pos_x + self.ghost_rect.width() >= max_x:
  515. pos_x = max_x - self.ghost_rect.width()
  516. elif pos_x > self.piano_width + self.ghost_rect.width()*3/4:
  517. pos_x -= self.ghost_rect.width()/2
  518. new_x, new_y = self.snap(pos_x, pos.y())
  519. self.ghost_rect.moveTo(new_x, new_y)
  520. self.ghost_note.setRect(self.ghost_rect)
  521. return
  522. if self.marquee_select:
  523. marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
  524. if marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() < pos.y():
  525. self.marquee_rect.setBottomRight(pos)
  526. elif marquee_orig_pos.x() < pos.x() and marquee_orig_pos.y() > pos.y():
  527. self.marquee_rect.setTopRight(pos)
  528. elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() < pos.y():
  529. self.marquee_rect.setBottomLeft(pos)
  530. elif marquee_orig_pos.x() > pos.x() and marquee_orig_pos.y() > pos.y():
  531. self.marquee_rect.setTopLeft(pos)
  532. self.marquee.setRect(self.marquee_rect)
  533. for note in self.selected_notes:
  534. note.setSelected(False)
  535. self.selected_notes = []
  536. for item in self.collidingItems(self.marquee):
  537. if item in self.notes:
  538. item.setSelected(True)
  539. self.selected_notes.append(item)
  540. return
  541. if event.buttons() != Qt.LeftButton:
  542. return
  543. if self.velocity_mode:
  544. for note in self.selected_notes:
  545. note.updateVelocity(event)
  546. return
  547. x = y = False
  548. for note in self.selected_notes:
  549. if note.back.stretch:
  550. x = True
  551. break
  552. for note in self.selected_notes:
  553. if note.front.stretch:
  554. y = True
  555. break
  556. for note in self.selected_notes:
  557. note.back.stretch = x
  558. note.front.stretch = y
  559. note.moveEvent(event)
  560. def mouseReleaseEvent(self, event):
  561. QGraphicsScene.mouseReleaseEvent(self, event)
  562. if self.ignore_mouse_events:
  563. self.ignore_mouse_events = False
  564. return
  565. if self.marquee_select:
  566. self.marquee_select = False
  567. self.removeItem(self.marquee)
  568. self.marquee = None
  569. if self.insert_mode and self.place_ghost:
  570. self.place_ghost = False
  571. note_start = self.get_note_start_from_x(self.ghost_rect.x())
  572. note_num = self.get_note_num_from_y(self.ghost_rect.y())
  573. note_length = self.get_note_length_from_x(self.ghost_rect.width())
  574. note = self.drawNote(note_num, note_start, note_length, self.ghost_vel)
  575. note.setSelected(True)
  576. self.selected_notes.append(note)
  577. self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
  578. pos = self.enforce_bounds(self.last_mouse_pos)
  579. pos_x = pos.x()
  580. if pos_x > self.piano_width + self.ghost_rect.width()*3/4:
  581. pos_x -= self.ghost_rect.width()/2
  582. self.makeGhostNote(pos_x, pos.y())
  583. for note in self.selected_notes:
  584. note.back.stretch = False
  585. note.front.stretch = False
  586. # -------------------------------------------------------------------------
  587. # Internal Functions
  588. def drawHeader(self):
  589. self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
  590. #self.header.setZValue(1.0)
  591. self.header.setPos(self.piano_width, 0)
  592. self.addItem(self.header)
  593. def drawPiano(self):
  594. piano_keys_width = self.piano_width - self.padding
  595. labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
  596. black_notes = (2,4,6,9,11)
  597. piano_label = QFont()
  598. piano_label.setPointSize(6)
  599. self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
  600. self.piano.setPos(0, self.header_height)
  601. self.addItem(self.piano)
  602. key = PianoKeyItem(piano_keys_width, self.note_height, 78, self.piano)
  603. label = QGraphicsSimpleTextItem('C9', key)
  604. label.setPos(18, 1)
  605. label.setFont(piano_label)
  606. key.setBrush(QColor(255, 255, 255))
  607. for i in range(self.end_octave - self.start_octave, 0, -1):
  608. for j in range(self.notes_in_octave, 0, -1):
  609. note = (self.end_octave - i + 3) * 12 - j
  610. if j in black_notes:
  611. key = PianoKeyItem(piano_keys_width/1.4, self.note_height, note, self.piano)
  612. key.setBrush(QColor(0, 0, 0))
  613. key.setZValue(1.0)
  614. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  615. elif (j - 1) and (j + 1) in black_notes:
  616. key = PianoKeyItem(piano_keys_width, self.note_height * 2, note, self.piano)
  617. key.setBrush(QColor(255, 255, 255))
  618. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  619. elif (j - 1) in black_notes:
  620. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
  621. key.setBrush(QColor(255, 255, 255))
  622. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  623. elif (j + 1) in black_notes:
  624. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
  625. key.setBrush(QColor(255, 255, 255))
  626. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  627. if j == 12:
  628. label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i + 1), key)
  629. label.setPos(18, 6)
  630. label.setFont(piano_label)
  631. self.piano_keys.append(key)
  632. def drawGrid(self):
  633. black_notes = [2,4,6,9,11]
  634. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  635. scale_bar.setPos(self.piano_width, 0)
  636. scale_bar.setBrush(QColor(100,100,100))
  637. clearpen = QPen(QColor(0,0,0,0))
  638. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  639. for j in range(self.notes_in_octave, 0, -1):
  640. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  641. scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
  642. scale_bar.setPen(clearpen)
  643. if j not in black_notes:
  644. scale_bar.setBrush(QColor(120,120,120))
  645. else:
  646. scale_bar.setBrush(QColor(100,100,100))
  647. measure_pen = QPen(QColor(0, 0, 0, 120), 3)
  648. half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
  649. line_pen = QPen(QColor(0, 0, 0, 40))
  650. for i in range(0, int(self.num_measures) + 1):
  651. measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
  652. measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
  653. measure.setPen(measure_pen)
  654. if i < self.num_measures:
  655. number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
  656. number.setPos(self.measure_width * i + 5, 2)
  657. number.setBrush(Qt.white)
  658. for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
  659. line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
  660. line.setZValue(1.0)
  661. line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
  662. if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
  663. line.setPen(half_measure_pen)
  664. else:
  665. line.setPen(line_pen)
  666. def drawPlayHead(self):
  667. self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
  668. self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
  669. self.play_head.setZValue(1.)
  670. self.addItem(self.play_head)
  671. def refreshScene(self):
  672. list(map(self.removeItem, self.notes))
  673. self.selected_notes = []
  674. self.piano_keys = []
  675. self.place_ghost = False
  676. if self.ghost_note is not None:
  677. self.removeItem(self.ghost_note)
  678. self.ghost_note = None
  679. self.clear()
  680. self.drawPiano()
  681. self.drawHeader()
  682. self.drawGrid()
  683. self.drawPlayHead()
  684. for note in self.notes[:]:
  685. if note.note[1] >= (self.num_measures * self.time_sig[0]):
  686. self.notes.remove(note)
  687. self.removed_notes.append(note)
  688. #self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  689. elif note.note[2] > self.max_note_length:
  690. new_note = note.note[:]
  691. new_note[2] = self.max_note_length
  692. self.notes.remove(note)
  693. self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
  694. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  695. self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
  696. for note in self.removed_notes[:]:
  697. if note.note[1] < (self.num_measures * self.time_sig[0]):
  698. self.removed_notes.remove(note)
  699. self.notes.append(note)
  700. list(map(self.addItem, self.notes))
  701. if self.views():
  702. self.views()[0].setSceneRect(self.itemsBoundingRect())
  703. def clearNotes(self):
  704. self.clear()
  705. self.notes = []
  706. self.removed_notes = []
  707. self.selected_notes = []
  708. self.drawPiano()
  709. self.drawHeader()
  710. self.drawGrid()
  711. def makeGhostNote(self, pos_x, pos_y):
  712. """creates the ghostnote that is placed on the scene before the real one is."""
  713. if self.ghost_note is not None:
  714. self.removeItem(self.ghost_note)
  715. length = self.full_note_width * self.default_length
  716. pos_x, pos_y = self.snap(pos_x, pos_y)
  717. self.ghost_vel = self.default_ghost_vel
  718. self.ghost_rect = QRectF(pos_x, pos_y, length, self.note_height)
  719. self.ghost_rect_orig_width = self.ghost_rect.width()
  720. self.ghost_note = QGraphicsRectItem(self.ghost_rect)
  721. self.ghost_note.setBrush(QColor(230, 221, 45, 100))
  722. self.addItem(self.ghost_note)
  723. def drawNote(self, note_num, note_start, note_length, note_velocity, add=True):
  724. """
  725. note_num: midi number, 0 - 127
  726. note_start: 0 - (num_measures * time_sig[0]) so this is in beats
  727. note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
  728. note_velocity: 0 - 127
  729. """
  730. info = [note_num, note_start, note_length, note_velocity]
  731. if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  732. #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
  733. while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  734. self.setMeasures(self.num_measures+1)
  735. self.measureupdate.emit(self.num_measures)
  736. self.refreshScene()
  737. x_start = self.get_note_x_start(note_start)
  738. if note_length > self.max_note_length:
  739. note_length = self.max_note_length + 0.25
  740. x_length = self.get_note_x_length(note_length)
  741. y_pos = self.get_note_y_pos(note_num)
  742. note = NoteItem(self.note_height, x_length, info)
  743. note.setPos(x_start, y_pos)
  744. self.notes.append(note)
  745. if add:
  746. self.addItem(note)
  747. return note
  748. # -------------------------------------------------------------------------
  749. # Helper Functions
  750. def frange(self, x, y, t):
  751. while x < y:
  752. yield x
  753. x += t
  754. def quantize(self, value):
  755. self.snap_value = float(self.full_note_width) * value if value else None
  756. def snap(self, pos_x, pos_y = None):
  757. if self.snap_value:
  758. pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) * self.snap_value + self.piano_width
  759. if pos_y is not None:
  760. pos_y = int((pos_y - self.header_height) / self.note_height) * self.note_height + self.header_height
  761. return (pos_x, pos_y) if pos_y is not None else pos_x
  762. def adjust_note_vel(self, event):
  763. m_pos = event.scenePos()
  764. #bind velocity to vertical mouse movement
  765. self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
  766. if self.ghost_vel < 0:
  767. self.ghost_vel = 0
  768. elif self.ghost_vel > 127:
  769. self.ghost_vel = 127
  770. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  771. if m_pos.x() < m_width:
  772. m_pos.setX(m_width)
  773. m_new_x = self.snap(m_pos.x())
  774. self.ghost_rect.setRight(m_new_x)
  775. self.ghost_note.setRect(self.ghost_rect)
  776. def enforce_bounds(self, pos):
  777. pos = QPointF(pos)
  778. if pos.x() < self.piano_width:
  779. pos.setX(self.piano_width)
  780. elif pos.x() >= self.grid_width + self.piano_width:
  781. pos.setX(self.grid_width + self.piano_width - 1)
  782. if pos.y() < self.header_height + self.padding:
  783. pos.setY(self.header_height + self.padding)
  784. return pos
  785. def get_note_start_from_x(self, note_x):
  786. return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
  787. def get_note_x_start(self, note_start):
  788. return self.piano_width + (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
  789. def get_note_x_length(self, note_length):
  790. return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
  791. def get_note_length_from_x(self, note_x):
  792. return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width * note_x
  793. def get_note_y_pos(self, note_num):
  794. return self.header_height + self.note_height * (self.total_notes - note_num - 1)
  795. def get_note_num_from_y(self, note_y_pos):
  796. return -(int((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
  797. def move_note(self, old_note, new_note):
  798. self.midievent.emit(["midievent-remove", old_note[0], old_note[1], old_note[2], old_note[3]])
  799. self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
  800. # ------------------------------------------------------------------------------------------------------------
  801. class PianoRollView(QGraphicsView):
  802. def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  803. QGraphicsView.__init__(self, parent)
  804. self.piano = PianoRoll(time_sig, num_measures, quantize_val)
  805. self.setScene(self.piano)
  806. #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  807. x = 0 * self.sceneRect().width() + self.sceneRect().left()
  808. y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
  809. self.centerOn(x, y)
  810. self.setAlignment(Qt.AlignLeft)
  811. self.o_transform = self.transform()
  812. self.zoom_x = 1
  813. self.zoom_y = 1
  814. def setZoomX(self, scale_x):
  815. self.setTransform(self.o_transform)
  816. self.zoom_x = 1 + scale_x / float(99) * 2
  817. self.scale(self.zoom_x, self.zoom_y)
  818. def setZoomY(self, scale_y):
  819. self.setTransform(self.o_transform)
  820. self.zoom_y = 1 + scale_y / float(99)
  821. self.scale(self.zoom_x, self.zoom_y)
  822. # ------------------------------------------------------------------------------------------------------------
  823. class ModeIndicator(QWidget):
  824. def __init__(self, parent):
  825. QWidget.__init__(self, parent)
  826. #self.setGeometry(0, 0, 30, 20)
  827. self.mode = None
  828. self.setFixedSize(30,20)
  829. def paintEvent(self, event):
  830. event.accept()
  831. painter = QPainter(self)
  832. painter.setPen(QPen(QColor(0, 0, 0, 0)))
  833. if self.mode == 'velocity_mode':
  834. painter.setBrush(QColor(127, 0, 0))
  835. elif self.mode == 'insert_mode':
  836. painter.setBrush(QColor(0, 100, 127))
  837. else:
  838. painter.setBrush(QColor(0, 0, 0, 0))
  839. painter.drawRect(0, 0, 30, 20)
  840. def changeMode(self, new_mode):
  841. self.mode = new_mode
  842. self.update()
  843. # ------------------------------------------------------------------------------------------------------------