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.

1021 lines
39KB

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