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 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # A piano roll viewer/editor
  4. # Copyright (C) 2012-2019 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, QFont, QPen, QPainter
  22. from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
  23. from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
  24. from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle
  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.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  66. self.setAcceptHoverEvents(True)
  67. clearpen = QPen(QColor(0,0,0,0))
  68. self.setPen(clearpen)
  69. self.orig_brush = QColor(0, 0, 0, 0)
  70. self.hover_brush = QColor(200, 200, 200)
  71. self.stretch = False
  72. def mousePressEvent(self, event):
  73. QGraphicsRectItem.mousePressEvent(self, event)
  74. self.stretch = True
  75. def hoverEnterEvent(self, event):
  76. QGraphicsRectItem.hoverEnterEvent(self, event)
  77. if self.parent.isSelected():
  78. self.parent.setBrush(self.parent.select_brush)
  79. else:
  80. self.parent.setBrush(self.parent.orig_brush)
  81. self.setBrush(self.hover_brush)
  82. def hoverLeaveEvent(self, event):
  83. QGraphicsRectItem.hoverLeaveEvent(self, event)
  84. if self.parent.isSelected():
  85. self.parent.setBrush(self.parent.select_brush)
  86. elif self.parent.hovering:
  87. self.parent.setBrush(self.parent.hover_brush)
  88. else:
  89. self.parent.setBrush(self.parent.orig_brush)
  90. self.setBrush(self.orig_brush)
  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.setFlag(QGraphicsItem.ItemIsMovable)
  96. self.setFlag(QGraphicsItem.ItemIsSelectable)
  97. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  98. self.setAcceptHoverEvents(True)
  99. clearpen = QPen(QColor(0,0,0,0))
  100. self.setPen(clearpen)
  101. self.orig_brush = QColor(note_info[3], 0, 0)
  102. self.hover_brush = QColor(note_info[3] + 100, 200, 100)
  103. self.select_brush = QColor(note_info[3] + 100, 100, 100)
  104. self.setBrush(self.orig_brush)
  105. self.note = note_info
  106. self.length = length
  107. self.piano = self.scene
  108. self.pressed = False
  109. self.hovering = False
  110. self.moving_diff = (0,0)
  111. self.expand_diff = 0
  112. l = 5
  113. self.front = NoteExpander(l, height, self)
  114. self.back = NoteExpander(l, height, self)
  115. self.back.setPos(length - l, 0)
  116. def paint(self, painter, option, widget=None):
  117. paint_option = option
  118. paint_option.state &= ~QStyle.State_Selected
  119. QGraphicsRectItem.paint(self, painter, paint_option, widget)
  120. def setSelected(self, boolean):
  121. QGraphicsRectItem.setSelected(self, boolean)
  122. if boolean: self.setBrush(self.select_brush)
  123. else: self.setBrush(self.orig_brush)
  124. def hoverEnterEvent(self, event):
  125. self.hovering = True
  126. QGraphicsRectItem.hoverEnterEvent(self, event)
  127. if not self.isSelected():
  128. self.setBrush(self.hover_brush)
  129. def hoverLeaveEvent(self, event):
  130. self.hovering = False
  131. QGraphicsRectItem.hoverLeaveEvent(self, event)
  132. if not self.isSelected():
  133. self.setBrush(self.orig_brush)
  134. elif self.isSelected():
  135. self.setBrush(self.select_brush)
  136. def mousePressEvent(self, event):
  137. QGraphicsRectItem.mousePressEvent(self, event)
  138. self.setSelected(True)
  139. self.pressed = True
  140. def mouseMoveEvent(self, event):
  141. pass
  142. def moveEvent(self, event):
  143. offset = event.scenePos() - event.lastScenePos()
  144. if self.back.stretch:
  145. self.expand(self.back, offset)
  146. else:
  147. self.move_pos = self.scenePos() + offset \
  148. + QPointF(self.moving_diff[0],self.moving_diff[1])
  149. pos = self.piano().enforce_bounds(self.move_pos)
  150. pos_x, pos_y = pos.x(), pos.y()
  151. pos_sx, pos_sy = self.piano().snap(pos_x, pos_y)
  152. self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
  153. if self.front.stretch:
  154. right = self.rect().right() - offset.x() + self.expand_diff
  155. if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \
  156. or right < 10:
  157. self.expand_diff = 0
  158. return
  159. self.expand(self.front, offset)
  160. self.setPos(pos_sx, self.scenePos().y())
  161. else:
  162. self.setPos(pos_sx, pos_sy)
  163. def expand(self, rectItem, offset):
  164. rect = self.rect()
  165. right = rect.right() + self.expand_diff
  166. if rectItem == self.back:
  167. right += offset.x()
  168. if right > self.piano().grid_width:
  169. right = self.piano().grid_width
  170. elif right < 10:
  171. right = 10
  172. new_x = self.piano().snap(right)
  173. else:
  174. right -= offset.x()
  175. new_x = self.piano().snap(right+2.75)
  176. if self.piano().snap_value: new_x -= 2.75 # where does this number come from?!
  177. self.expand_diff = right - new_x
  178. self.back.setPos(new_x - 5, 0)
  179. rect.setRight(new_x)
  180. self.setRect(rect)
  181. def updateNoteInfo(self, pos_x, pos_y):
  182. self.note[0] = self.piano().get_note_num_from_y(pos_y)
  183. self.note[1] = self.piano().get_note_start_from_x(pos_x)
  184. self.note[2] = self.piano().get_note_length_from_x(
  185. self.rect().right() - self.rect().left())
  186. #print("note: {}".format(self.note))
  187. def mouseReleaseEvent(self, event):
  188. QGraphicsRectItem.mouseReleaseEvent(self, event)
  189. self.pressed = False
  190. if event.button() == Qt.LeftButton:
  191. self.moving_diff = (0,0)
  192. self.expand_diff = 0
  193. self.back.stretch = False
  194. self.front.stretch = False
  195. (pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y())
  196. self.setPos(pos_x, pos_y)
  197. self.updateNoteInfo(pos_x, pos_y)
  198. def updateVelocity(self, event):
  199. offset = event.scenePos().x() - event.lastScenePos().x()
  200. self.note[3] += int(offset/5)
  201. if self.note[3] > 127:
  202. self.note[3] = 127
  203. elif self.note[3] < 0:
  204. self.note[3] = 0
  205. print("new vel: {}".format(self.note[3]))
  206. self.orig_brush = QColor(self.note[3], 0, 0)
  207. self.select_brush = QColor(self.note[3] + 100, 100, 100)
  208. self.setBrush(self.orig_brush)
  209. class PianoKeyItem(QGraphicsRectItem):
  210. def __init__(self, width, height, parent):
  211. QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
  212. self.setPen(QPen(QColor(0,0,0,80)))
  213. self.width = width
  214. self.height = height
  215. self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
  216. self.setAcceptHoverEvents(True)
  217. self.hover_brush = QColor(200, 0, 0)
  218. self.click_brush = QColor(255, 100, 100)
  219. self.pressed = False
  220. def hoverEnterEvent(self, event):
  221. QGraphicsRectItem.hoverEnterEvent(self, event)
  222. self.orig_brush = self.brush()
  223. self.setBrush(self.hover_brush)
  224. def hoverLeaveEvent(self, event):
  225. if self.pressed:
  226. self.pressed = False
  227. self.setBrush(self.hover_brush)
  228. QGraphicsRectItem.hoverLeaveEvent(self, event)
  229. self.setBrush(self.orig_brush)
  230. #def mousePressEvent(self, event):
  231. # self.pressed = True
  232. # self.setBrush(self.click_brush)
  233. def mouseMoveEvent(self, event):
  234. """this may eventually do something"""
  235. pass
  236. def mouseReleaseEvent(self, event):
  237. self.pressed = False
  238. QGraphicsRectItem.mouseReleaseEvent(self, event)
  239. self.setBrush(self.hover_brush)
  240. class PianoRoll(QGraphicsScene):
  241. '''the piano roll'''
  242. midievent = pyqtSignal(list)
  243. measureupdate = pyqtSignal(int)
  244. modeupdate = pyqtSignal(str)
  245. def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  246. QGraphicsScene.__init__(self)
  247. self.setBackgroundBrush(QColor(50, 50, 50))
  248. self.mousePos = QPointF()
  249. self.notes = []
  250. self.selected_notes = []
  251. self.piano_keys = []
  252. self.marquee_select = False
  253. self.insert_mode = False
  254. self.velocity_mode = False
  255. self.place_ghost = False
  256. self.ghost_note = None
  257. self.default_ghost_vel = 100
  258. self.ghost_vel = self.default_ghost_vel
  259. ## dimensions
  260. self.padding = 2
  261. ## piano dimensions
  262. self.note_height = 10
  263. self.start_octave = -2
  264. self.end_octave = 8
  265. self.notes_in_octave = 12
  266. self.total_notes = (self.end_octave - self.start_octave) \
  267. * self.notes_in_octave + 1
  268. self.piano_height = self.note_height * self.total_notes
  269. self.octave_height = self.notes_in_octave * self.note_height
  270. self.piano_width = 34
  271. ## height
  272. self.header_height = 20
  273. self.total_height = self.piano_height - self.note_height + self.header_height
  274. #not sure why note_height is subtracted
  275. ## width
  276. self.full_note_width = 250 # i.e. a 4/4 note
  277. self.snap_value = None
  278. self.quantize_val = quantize_val
  279. ### dummy vars that will be changed
  280. self.time_sig = 0
  281. self.measure_width = 0
  282. self.num_measures = 0
  283. self.max_note_length = 0
  284. self.grid_width = 0
  285. self.value_width = 0
  286. self.grid_div = 0
  287. self.piano = None
  288. self.header = None
  289. self.play_head = None
  290. self.setTimeSig(time_sig)
  291. self.setMeasures(num_measures)
  292. self.setGridDiv()
  293. self.default_length = 1. / self.grid_div
  294. # -------------------------------------------------------------------------
  295. # Callbacks
  296. def movePlayHead(self, transport_info):
  297. # TODO: need conversion between frames and PPQ
  298. x = 105. # works for 120bpm
  299. total_duration = self.time_sig[0] * self.num_measures * x
  300. pos = transport_info['frame'] / x
  301. frac = (pos % total_duration) / total_duration
  302. self.play_head.setPos(QPointF(frac * self.grid_width, 0))
  303. def setTimeSig(self, time_sig):
  304. try:
  305. new_time_sig = list(map(float, time_sig.split('/')))
  306. if len(new_time_sig)==2:
  307. self.time_sig = new_time_sig
  308. self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
  309. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  310. self.grid_width = self.measure_width * self.num_measures
  311. self.setGridDiv()
  312. except ValueError:
  313. pass
  314. def setMeasures(self, measures):
  315. try:
  316. self.num_measures = float(measures)
  317. self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
  318. self.grid_width = self.measure_width * self.num_measures
  319. self.refreshScene()
  320. except:
  321. pass
  322. def setDefaultLength(self, length):
  323. try:
  324. v = list(map(float, length.split('/')))
  325. if len(v) < 3:
  326. self.default_length = \
  327. v[0] if len(v)==1 else \
  328. v[0] / v[1]
  329. pos = self.enforce_bounds(self.mousePos)
  330. if self.insert_mode: self.makeGhostNote(pos.x(), pos.y())
  331. except ValueError:
  332. pass
  333. def setGridDiv(self, div=None):
  334. if not div: div = self.quantize_val
  335. try:
  336. val = list(map(int, div.split('/')))
  337. if len(val) < 3:
  338. self.quantize_val = div
  339. self.grid_div = val[0] if len(val)==1 else val[1]
  340. self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
  341. self.setQuantize(div)
  342. self.refreshScene()
  343. except ValueError:
  344. pass
  345. def setQuantize(self, value):
  346. try:
  347. val = list(map(float, value.split('/')))
  348. if len(val) == 1:
  349. self.quantize(val[0])
  350. self.quantize_val = value
  351. elif len(val) == 2:
  352. self.quantize(val[0] / val[1])
  353. self.quantize_val = value
  354. except ValueError:
  355. pass
  356. # -------------------------------------------------------------------------
  357. # Event Callbacks
  358. def keyPressEvent(self, event):
  359. QGraphicsScene.keyPressEvent(self, event)
  360. if event.key() == Qt.Key_F:
  361. if not self.insert_mode:
  362. self.velocity_mode = False
  363. self.insert_mode = True
  364. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  365. self.modeupdate.emit('insert_mode')
  366. elif self.insert_mode:
  367. self.insert_mode = False
  368. if self.place_ghost: self.place_ghost = False
  369. self.removeItem(self.ghost_note)
  370. self.ghost_note = None
  371. self.modeupdate.emit('')
  372. elif event.key() == Qt.Key_D:
  373. if self.velocity_mode:
  374. self.velocity_mode = False
  375. self.modeupdate.emit('')
  376. else:
  377. if self.insert_mode:
  378. self.removeItem(self.ghost_note)
  379. self.ghost_note = None
  380. self.insert_mode = False
  381. self.place_ghost = False
  382. self.velocity_mode = True
  383. self.modeupdate.emit('velocity_mode')
  384. elif event.key() == Qt.Key_A:
  385. if all((note.isSelected() for note in self.notes)):
  386. for note in self.notes:
  387. note.setSelected(False)
  388. self.selected_notes = []
  389. else:
  390. for note in self.notes:
  391. note.setSelected(True)
  392. self.selected_notes = self.notes[:]
  393. elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
  394. self.notes = [note for note in self.notes if note not in self.selected_notes]
  395. for note in self.selected_notes:
  396. self.removeItem(note)
  397. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  398. del note
  399. self.selected_notes = []
  400. def mousePressEvent(self, event):
  401. QGraphicsScene.mousePressEvent(self, event)
  402. if not (any(key.pressed for key in self.piano_keys)
  403. or any(note.pressed for note in self.notes)):
  404. for note in self.selected_notes:
  405. note.setSelected(False)
  406. self.selected_notes = []
  407. if event.button() == Qt.LeftButton:
  408. if self.insert_mode:
  409. self.place_ghost = True
  410. else:
  411. self.marquee_select = True
  412. self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
  413. self.marquee = QGraphicsRectItem(self.marquee_rect)
  414. self.marquee.setBrush(QColor(255, 255, 255, 100))
  415. self.addItem(self.marquee)
  416. else:
  417. for s_note in self.notes:
  418. if s_note.pressed and s_note in self.selected_notes:
  419. break
  420. elif s_note.pressed and s_note not in self.selected_notes:
  421. for note in self.selected_notes:
  422. note.setSelected(False)
  423. self.selected_notes = [s_note]
  424. break
  425. for note in self.selected_notes:
  426. if not self.velocity_mode:
  427. note.mousePressEvent(event)
  428. def mouseMoveEvent(self, event):
  429. QGraphicsScene.mouseMoveEvent(self, event)
  430. self.mousePos = event.scenePos()
  431. if not (any((key.pressed for key in self.piano_keys))):
  432. m_pos = event.scenePos()
  433. if self.insert_mode and self.place_ghost: #placing a note
  434. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  435. if m_pos.x() > m_width:
  436. m_new_x = self.snap(m_pos.x())
  437. self.ghost_rect.setRight(m_new_x)
  438. self.ghost_note.setRect(self.ghost_rect)
  439. #self.adjust_note_vel(event)
  440. else:
  441. m_pos = self.enforce_bounds(m_pos)
  442. if self.insert_mode: #ghostnote follows mouse around
  443. (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y())
  444. self.ghost_rect.moveTo(m_new_x, m_new_y)
  445. try:
  446. self.ghost_note.setRect(self.ghost_rect)
  447. except RuntimeError:
  448. self.ghost_note = None
  449. self.makeGhostNote(m_new_x, m_new_y)
  450. elif self.marquee_select:
  451. marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
  452. if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  453. self.marquee_rect.setBottomRight(m_pos)
  454. elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  455. self.marquee_rect.setTopRight(m_pos)
  456. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y():
  457. self.marquee_rect.setBottomLeft(m_pos)
  458. elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y():
  459. self.marquee_rect.setTopLeft(m_pos)
  460. self.marquee.setRect(self.marquee_rect)
  461. self.selected_notes = []
  462. for item in self.collidingItems(self.marquee):
  463. if item in self.notes:
  464. self.selected_notes.append(item)
  465. for note in self.notes:
  466. if note in self.selected_notes: note.setSelected(True)
  467. else: note.setSelected(False)
  468. elif self.velocity_mode:
  469. if Qt.LeftButton == event.buttons():
  470. for note in self.selected_notes:
  471. note.updateVelocity(event)
  472. elif not self.marquee_select: #move selected
  473. if Qt.LeftButton == event.buttons():
  474. x = y = False
  475. if any(note.back.stretch for note in self.selected_notes):
  476. x = True
  477. elif any(note.front.stretch for note in self.selected_notes):
  478. y = True
  479. for note in self.selected_notes:
  480. note.back.stretch = x
  481. note.front.stretch = y
  482. note.moveEvent(event)
  483. def mouseReleaseEvent(self, event):
  484. if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
  485. if event.button() == Qt.LeftButton:
  486. if self.place_ghost and self.insert_mode:
  487. self.place_ghost = False
  488. note_start = self.get_note_start_from_x(self.ghost_rect.x())
  489. note_num = self.get_note_num_from_y(self.ghost_rect.y())
  490. note_length = self.get_note_length_from_x(self.ghost_rect.width())
  491. self.drawNote(note_num, note_start, note_length, self.ghost_vel)
  492. self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
  493. self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
  494. elif self.marquee_select:
  495. self.marquee_select = False
  496. self.removeItem(self.marquee)
  497. elif not self.marquee_select:
  498. for note in self.selected_notes:
  499. old_info = note.note[:]
  500. note.mouseReleaseEvent(event)
  501. if self.velocity_mode:
  502. note.setSelected(True)
  503. if not old_info == note.note:
  504. self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]])
  505. self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]])
  506. # -------------------------------------------------------------------------
  507. # Internal Functions
  508. def drawHeader(self):
  509. self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
  510. #self.header.setZValue(1.0)
  511. self.header.setPos(self.piano_width, 0)
  512. self.addItem(self.header)
  513. def drawPiano(self):
  514. piano_keys_width = self.piano_width - self.padding
  515. labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
  516. black_notes = (2,4,6,9,11)
  517. piano_label = QFont()
  518. piano_label.setPointSize(6)
  519. self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
  520. self.piano.setPos(0, self.header_height)
  521. self.addItem(self.piano)
  522. key = PianoKeyItem(piano_keys_width, self.note_height, self.piano)
  523. label = QGraphicsSimpleTextItem('C8', key)
  524. label.setPos(18, 1)
  525. label.setFont(piano_label)
  526. key.setBrush(QColor(255, 255, 255))
  527. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  528. for j in range(self.notes_in_octave, 0, -1):
  529. if j in black_notes:
  530. key = PianoKeyItem(piano_keys_width/1.4, self.note_height, self.piano)
  531. key.setBrush(QColor(0, 0, 0))
  532. key.setZValue(1.0)
  533. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  534. elif (j - 1) and (j + 1) in black_notes:
  535. key = PianoKeyItem(piano_keys_width, self.note_height * 2, self.piano)
  536. key.setBrush(QColor(255, 255, 255))
  537. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  538. elif (j - 1) in black_notes:
  539. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  540. key.setBrush(QColor(255, 255, 255))
  541. key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
  542. elif (j + 1) in black_notes:
  543. key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, self.piano)
  544. key.setBrush(QColor(255, 255, 255))
  545. key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
  546. if j == 12:
  547. label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1], self.end_octave - i), key )
  548. label.setPos(18, 6)
  549. label.setFont(piano_label)
  550. self.piano_keys.append(key)
  551. def drawGrid(self):
  552. black_notes = [2,4,6,9,11]
  553. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  554. scale_bar.setPos(self.piano_width, 0)
  555. scale_bar.setBrush(QColor(100,100,100))
  556. clearpen = QPen(QColor(0,0,0,0))
  557. for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
  558. for j in range(self.notes_in_octave, 0, -1):
  559. scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
  560. scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
  561. scale_bar.setPen(clearpen)
  562. if j not in black_notes:
  563. scale_bar.setBrush(QColor(120,120,120))
  564. else:
  565. scale_bar.setBrush(QColor(100,100,100))
  566. measure_pen = QPen(QColor(0, 0, 0, 120), 3)
  567. half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
  568. line_pen = QPen(QColor(0, 0, 0, 40))
  569. for i in range(0, int(self.num_measures) + 1):
  570. measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
  571. measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
  572. measure.setPen(measure_pen)
  573. if i < self.num_measures:
  574. number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
  575. number.setPos(self.measure_width * i + 5, 2)
  576. number.setBrush(Qt.white)
  577. for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
  578. line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
  579. line.setZValue(1.0)
  580. line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
  581. if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
  582. line.setPen(half_measure_pen)
  583. else:
  584. line.setPen(line_pen)
  585. def drawPlayHead(self):
  586. self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
  587. self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
  588. self.play_head.setZValue(1.)
  589. self.addItem(self.play_head)
  590. def refreshScene(self):
  591. list(map(self.removeItem, self.notes))
  592. self.selected_notes = []
  593. self.piano_keys = []
  594. self.clear()
  595. self.drawPiano()
  596. self.drawHeader()
  597. self.drawGrid()
  598. self.drawPlayHead()
  599. for note in self.notes[:]:
  600. if note.note[1] >= (self.num_measures * self.time_sig[0]):
  601. self.notes.remove(note)
  602. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  603. elif note.note[2] > self.max_note_length:
  604. new_note = note.note[:]
  605. new_note[2] = self.max_note_length
  606. self.notes.remove(note)
  607. self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
  608. self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
  609. self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])
  610. list(map(self.addItem, self.notes))
  611. if self.views():
  612. self.views()[0].setSceneRect(self.itemsBoundingRect())
  613. def clearNotes(self):
  614. self.clear()
  615. self.notes = []
  616. self.selected_notes = []
  617. self.drawPiano()
  618. self.drawHeader()
  619. self.drawGrid()
  620. def makeGhostNote(self, pos_x, pos_y):
  621. """creates the ghostnote that is placed on the scene before the real one is."""
  622. if self.ghost_note:
  623. self.removeItem(self.ghost_note)
  624. length = self.full_note_width * self.default_length
  625. (start, note) = self.snap(pos_x, pos_y)
  626. self.ghost_vel = self.default_ghost_vel
  627. self.ghost_rect = QRectF(start, note, length, self.note_height)
  628. self.ghost_rect_orig_width = self.ghost_rect.width()
  629. self.ghost_note = QGraphicsRectItem(self.ghost_rect)
  630. self.ghost_note.setBrush(QColor(230, 221, 45, 100))
  631. self.addItem(self.ghost_note)
  632. def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True):
  633. """
  634. note_num: midi number, 0 - 127
  635. note_start: 0 - (num_measures * time_sig[0]) so this is in beats
  636. note_length: 0 - (num_measures * time_sig[0]/time_sig[1]) this is in measures
  637. note_velocity: 0 - 127
  638. """
  639. info = [note_num, note_start, note_length, note_velocity]
  640. if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  641. #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
  642. while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
  643. self.setMeasures(self.num_measures+1)
  644. self.measureupdate.emit(self.num_measures)
  645. self.refreshScene()
  646. x_start = self.get_note_x_start(note_start)
  647. if note_length > self.max_note_length:
  648. note_length = self.max_note_length + 0.25
  649. x_length = self.get_note_x_length(note_length)
  650. y_pos = self.get_note_y_pos(note_num)
  651. note = NoteItem(self.note_height, x_length, info)
  652. note.setPos(x_start, y_pos)
  653. self.notes.append(note)
  654. if add:
  655. self.addItem(note)
  656. # -------------------------------------------------------------------------
  657. # Helper Functions
  658. def frange(self, x, y, t):
  659. while x < y:
  660. yield x
  661. x += t
  662. def quantize(self, value):
  663. self.snap_value = float(self.full_note_width) * value if value else None
  664. def snap(self, pos_x, pos_y = None):
  665. if self.snap_value:
  666. pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \
  667. * self.snap_value + self.piano_width
  668. if pos_y:
  669. pos_y = int((pos_y - self.header_height) / self.note_height) \
  670. * self.note_height + self.header_height
  671. return (pos_x, pos_y) if pos_y else pos_x
  672. def adjust_note_vel(self, event):
  673. m_pos = event.scenePos()
  674. #bind velocity to vertical mouse movement
  675. self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
  676. if self.ghost_vel < 0:
  677. self.ghost_vel = 0
  678. elif self.ghost_vel > 127:
  679. self.ghost_vel = 127
  680. m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
  681. if m_pos.x() < m_width:
  682. m_pos.setX(m_width)
  683. m_new_x = self.snap(m_pos.x())
  684. self.ghost_rect.setRight(m_new_x)
  685. self.ghost_note.setRect(self.ghost_rect)
  686. def enforce_bounds(self, pos):
  687. if pos.x() < self.piano_width:
  688. pos.setX(self.piano_width)
  689. elif pos.x() > self.grid_width + self.piano_width:
  690. pos.setX(self.grid_width + self.piano_width)
  691. if pos.y() < self.header_height + self.padding:
  692. pos.setY(self.header_height + self.padding)
  693. return pos
  694. def get_note_start_from_x(self, note_x):
  695. return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])
  696. def get_note_x_start(self, note_start):
  697. return self.piano_width + \
  698. (self.grid_width / self.num_measures / self.time_sig[0]) * note_start
  699. def get_note_x_length(self, note_length):
  700. return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures
  701. def get_note_length_from_x(self, note_x):
  702. return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \
  703. * note_x
  704. def get_note_y_pos(self, note_num):
  705. return self.header_height + self.note_height * (self.total_notes - note_num - 1)
  706. def get_note_num_from_y(self, note_y_pos):
  707. return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)
  708. class PianoRollView(QGraphicsView):
  709. def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
  710. QGraphicsView.__init__(self, parent)
  711. self.piano = PianoRoll(time_sig, num_measures, quantize_val)
  712. self.setScene(self.piano)
  713. #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  714. x = 0 * self.sceneRect().width() + self.sceneRect().left()
  715. y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
  716. self.centerOn(x, y)
  717. self.setAlignment(Qt.AlignLeft)
  718. self.o_transform = self.transform()
  719. self.zoom_x = 1
  720. self.zoom_y = 1
  721. def setZoomX(self, scale_x):
  722. self.setTransform(self.o_transform)
  723. self.zoom_x = 1 + scale_x / float(99) * 2
  724. self.scale(self.zoom_x, self.zoom_y)
  725. def setZoomY(self, scale_y):
  726. self.setTransform(self.o_transform)
  727. self.zoom_y = 1 + scale_y / float(99)
  728. self.scale(self.zoom_x, self.zoom_y)
  729. # ------------------------------------------------------------------------------------------------------------
  730. class ModeIndicator(QWidget):
  731. def __init__(self, parent):
  732. QWidget.__init__(self, parent)
  733. #self.setGeometry(0, 0, 30, 20)
  734. self.setFixedSize(30,20)
  735. self.mode = None
  736. def paintEvent(self, event):
  737. painter = QPainter(self)
  738. event.accept()
  739. painter.begin(self)
  740. painter.setPen(QPen(QColor(0, 0, 0, 0)))
  741. if self.mode == 'velocity_mode':
  742. painter.setBrush(QColor(127, 0, 0))
  743. elif self.mode == 'insert_mode':
  744. painter.setBrush(QColor(0, 100, 127))
  745. else:
  746. painter.setBrush(QColor(0, 0, 0, 0))
  747. painter.drawRect(0, 0, 30, 20)
  748. # FIXME
  749. painter.end()
  750. def changeMode(self, new_mode):
  751. self.mode = new_mode
  752. self.update()
  753. # ------------------------------------------------------------------------------------------------------------