Compare commits

...

20 Commits

Author SHA1 Message Date
Ben Niemann 6d12ce433a Increase project length. 2 years ago
Ben Niemann 95ba1e37c3 Merge branch 'master' into tracklist 2 years ago
Ben Niemann 010cf5d1ad Don't build csound STK opcodes to avoid log spam. 2 years ago
Ben Niemann 803875a33a Reduce log spam. 2 years ago
Ben Niemann c244c0eee7 Fix potential memory corruption. 3 years ago
Ben Niemann e108a3c53b Use (slightly optimized) common method to draw the time grid on tracks. 3 years ago
Ben Niemann 4997f9ca50 Show label with track name. 3 years ago
Ben Niemann ccb332db72 Add a signal to track the global mouse pointer position. 3 years ago
Ben Niemann 29d100f649 Fix "RuntimeWarning: 'noisicaa.core.process_manager' found in sys.modules after import of package 'noisicaa.core', but prior to execution of 'noisicaa.core.process_manager'; this may result in unpredictable behaviour" 3 years ago
Ben Niemann 15a0428ffe Correctly handle EnvBuilder failures. 3 years ago
Ben Niemann 5941e8f95c Make track list sortable. 3 years ago
Ben Niemann b173c4995f Handle move mutations for node list. 3 years ago
Ben Niemann 8688772da2 Add a move() method to list properties. 3 years ago
Ben Niemann 41cf0605df Resizable tracks and zoom in track list. 3 years ago
Ben Niemann d2071a2ff4 Show pianoroll keys only when track is active. 3 years ago
Ben Niemann 17197081e8 Editor widget directly owns (and positions) the track widgets (without using a QLayout). 3 years ago
Ben Niemann 2b3f402d91 Don't crash when sending message to unknown node. 3 years ago
Ben Niemann 3c287551d4 Factor out mapping a list of wrappers for object lists. 3 years ago
Ben Niemann 1586e3bc76 Typing improvements. 3 years ago
Ben Niemann 4180184011 Create projects filled with randomness, based on a JSON formatted spec. 3 years ago
  1. 11
      3rdparty/typeshed/PyQt5/QtCore.pyi
  2. 11
      3rdparty/typeshed/PyQt5/QtWidgets.pyi
  3. 2
      3rdparty/typeshed/fastjsonschema.pyi
  4. 6
      build_utils/waf/virtenv.py
  5. 4
      noisicaa/audioproc/engine/fluidsynth_util.cpp
  6. 6
      noisicaa/audioproc/engine/realm.pyx
  7. 12
      noisicaa/audioproc/public/musical_time.pyx
  8. 6
      noisicaa/builtin_nodes/beat_track/track_ui.py
  9. 20
      noisicaa/builtin_nodes/control_track/track_ui.py
  10. 93
      noisicaa/builtin_nodes/pianoroll_track/track_ui.py
  11. 2
      noisicaa/builtin_nodes/sample_track/track_ui.py
  12. 6
      noisicaa/builtin_nodes/score_track/track_ui.py
  13. 12
      noisicaa/builtin_nodes/step_sequencer/node_ui.py
  14. 230
      noisicaa/core/process_manager.py
  15. 133
      noisicaa/core/process_manager_entry.py
  16. 127
      noisicaa/core/process_manager_io.py
  17. 2
      noisicaa/core/wscript
  18. 2
      noisicaa/music/__init__.py
  19. 8
      noisicaa/music/base_track.py
  20. 8
      noisicaa/music/graph.py
  21. 303
      noisicaa/music/loadtest_generator.py
  22. 59
      noisicaa/music/loadtest_generator_test.py
  23. 33
      noisicaa/music/model_base.py
  24. 17
      noisicaa/music/model_base_test.py
  25. 8
      noisicaa/music/mutations.proto
  26. 18
      noisicaa/music/mutations.py
  27. 4
      noisicaa/music/player.py
  28. 13
      noisicaa/music/project.py
  29. 17
      noisicaa/music/project_client.py
  30. 2
      noisicaa/music/wscript
  31. 25
      noisicaa/ui/base_dial.py
  32. 24
      noisicaa/ui/clipboard.py
  33. 127
      noisicaa/ui/code_editor.py
  34. 2
      noisicaa/ui/control_value_dial.py
  35. 13
      noisicaa/ui/editor_app.py
  36. 32
      noisicaa/ui/editor_window.py
  37. 15
      noisicaa/ui/graph/canvas.py
  38. 2
      noisicaa/ui/graph/view.py
  39. 2
      noisicaa/ui/instrument_list.py
  40. 4
      noisicaa/ui/int_dial.py
  41. 7
      noisicaa/ui/object_list_editor.py
  42. 157
      noisicaa/ui/object_list_manager.py
  43. 144
      noisicaa/ui/open_project_dialog.py
  44. 1
      noisicaa/ui/pianoroll.py
  45. 19
      noisicaa/ui/project_registry.py
  46. 36
      noisicaa/ui/qtyping.py
  47. 30
      noisicaa/ui/settings_dialog.py
  48. 32
      noisicaa/ui/slots.py
  49. 35
      noisicaa/ui/track_list/base_track_editor.py
  50. 607
      noisicaa/ui/track_list/editor.py
  51. 15
      noisicaa/ui/track_list/measured_track_editor.py
  52. 22
      noisicaa/ui/track_list/time_line.py
  53. 99
      noisicaa/ui/track_list/time_view_mixin.py
  54. 5
      noisicaa/ui/track_list/view.py
  55. 4
      noisicaa/ui/ui_base.py
  56. 3
      noisicaa/ui/wscript

11
3rdparty/typeshed/PyQt5/QtCore.pyi vendored

@ -1696,26 +1696,27 @@ class QObject(sip.wrapper):
def isWidgetType(self) -> bool: ...
def setObjectName(self, name: str) -> None: ...
def objectName(self) -> str: ...
O = typing.TypeVar('O', bound='QObject')
@typing.overload
def findChildren(self, type: type, name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
def findChildren(self, type: typing.Type[O], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[O]: ...
@typing.overload
def findChildren(self, types: typing.Tuple, name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
@typing.overload
def findChildren(self, type: type, regExp: 'QRegExp', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
def findChildren(self, type: typing.Type[O], regExp: 'QRegExp', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[O]: ...
@typing.overload
def findChildren(self, types: typing.Tuple, regExp: 'QRegExp', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
@typing.overload
def findChildren(self, type: type, re: 'QRegularExpression', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
def findChildren(self, type: typing.Type[O], re: 'QRegularExpression', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List[O]: ...
@typing.overload
def findChildren(self, types: typing.Tuple, re: 'QRegularExpression', options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> typing.List['QObject']: ...
@typing.overload
def findChild(self, type: type, name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> 'QObject': ...
def findChild(self, type: typing.Type[O], name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> O: ...
@typing.overload
def findChild(self, types: typing.Tuple, name: str = ..., options: typing.Union[Qt.FindChildOptions, Qt.FindChildOption] = ...) -> 'QObject': ...
def tr(self, sourceText: str, disambiguation: typing.Optional[str] = ..., n: int = ...) -> str: ...
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool: ...
def event(self, a0: 'QEvent') -> bool: ...
def __getattr__(self, name: str) -> typing.Any: ...
#def __getattr__(self, name: str) -> typing.Any: ...
def pyqtConfigure(self, a0: typing.Any) -> None: ...
def metaObject(self) -> 'QMetaObject': ...

11
3rdparty/typeshed/PyQt5/QtWidgets.pyi vendored

@ -1006,6 +1006,8 @@ class QAction(QtCore.QObject):
def setFont(self, font: QtGui.QFont) -> None: ...
def shortcutContext(self) -> QtCore.Qt.ShortcutContext: ...
def setShortcutContext(self, context: QtCore.Qt.ShortcutContext) -> None: ...
def shortcutVisibleInContextMenu(self) -> bool: ...
def setShortcutVisibleInContextMenu(self, visible: bool) -> None: ...
def shortcut(self) -> QtGui.QKeySequence: ...
def setShortcut(self, shortcut: typing.Union[QtGui.QKeySequence, QtGui.QKeySequence.StandardKey, str, int]) -> None: ...
def isSeparator(self) -> bool: ...
@ -6568,6 +6570,10 @@ class QOpenGLWidget(QWidget):
class QPlainTextEdit(QAbstractScrollArea):
blockCountChanged = ... # type: PYQT_SIGNAL
updateRequest = ... # type: PYQT_SIGNAL
cursorPositionChanged = ... # type: PYQT_SIGNAL
textChanged = ... # type: PYQT_SIGNAL
class LineWrapMode(int): ...
NoWrap = ... # type: 'QPlainTextEdit.LineWrapMode'
@ -6619,14 +6625,10 @@ class QPlainTextEdit(QAbstractScrollArea):
def timerEvent(self, e: QtCore.QTimerEvent) -> None: ...
def event(self, e: QtCore.QEvent) -> bool: ...
def modificationChanged(self, a0: bool) -> None: ...
def blockCountChanged(self, newBlockCount: int) -> None: ...
def updateRequest(self, rect: QtCore.QRect, dy: int) -> None: ...
def cursorPositionChanged(self) -> None: ...
def selectionChanged(self) -> None: ...
def copyAvailable(self, b: bool) -> None: ...
def redoAvailable(self, b: bool) -> None: ...
def undoAvailable(self, b: bool) -> None: ...
def textChanged(self) -> None: ...
def centerCursor(self) -> None: ...
def appendHtml(self, html: str) -> None: ...
def appendPlainText(self, text: str) -> None: ...
@ -7208,6 +7210,7 @@ class QSpinBox(QAbstractSpinBox):
class QDoubleSpinBox(QAbstractSpinBox):
valueChanged = ... # type: PYQT_SIGNAL
def __init__(self, parent: typing.Optional[QWidget] = ...) -> None: ...

2
3rdparty/typeshed/fastjsonschema.pyi vendored

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

6
build_utils/waf/virtenv.py

@ -165,6 +165,7 @@ def configure(ctx):
pip_mgr.check_package(RUNTIME, 'sortedcontainers')
pip_mgr.check_package(RUNTIME, 'toposort')
pip_mgr.check_package(RUNTIME, 'urwid')
pip_mgr.check_package(RUNTIME, 'fastjsonschema')
pip_mgr.check_package(BUILD, 'cssutils')
pip_mgr.check_package(BUILD, 'Cython', version='0.29.6')
pip_mgr.check_package(BUILD, 'Jinja2')
@ -328,9 +329,9 @@ def check_virtual_env(ctx):
system_site_packages=False,
with_pip=True)
env_builder.create(venvdir)
except Exception as exc: # pylint: disable=broad-except
except BaseException as exc: # pylint: disable=broad-except
shutil.rmtree(venvdir)
ctx.fatal("Failed to create virtual env: %s" % exc)
ctx.fatal("Failed to create virtual env: %s: %s" % (type(exc).__name__, exc))
# Always update PIP to something more recent than what ensurepip has installed. We need at
# least 9.0 for 'pip list --format=json' to work.
@ -701,6 +702,7 @@ class CSoundBuilder(ThirdPartyBuilder):
['cmake',
'-DBUILD_PYTHON_INTERFACE=0',
'-DBUILD_LINEAR_ALGEBRA_OPCODES=0',
'-DBUILD_STK_OPCODES=0',
'-DCMAKE_INSTALL_PREFIX=' + self._ctx.env.VIRTUAL_ENV,
os.path.abspath(src_path)
],

4
noisicaa/audioproc/engine/fluidsynth_util.cpp

@ -162,12 +162,12 @@ Status FluidSynthUtil::process_block(
if ((midi[0] & 0xf0) == 0x90) {
int rc = fluid_synth_noteon(_synth, 0, midi[1], midi[2]);
if (rc == FLUID_FAILED) {
_logger->warning("noteon failed.");
_logger->info("noteon failed.");
}
} else if ((midi[0] & 0xf0) == 0x80) {
int rc = fluid_synth_noteoff(_synth, 0, midi[1]);
if (rc == FLUID_FAILED) {
_logger->warning("noteoff failed.");
_logger->info("noteoff failed.");
}
} else {
_logger->warning("Ignoring unsupported midi event %d.", midi[0] & 0xf0);

6
noisicaa/audioproc/engine/realm.pyx

@ -233,7 +233,11 @@ cdef class PyRealm(object):
node.set_session_value(node_key, session_value)
def send_node_message(self, msg):
node = self.__graph.find_node(msg.node_id)
try:
node = self.__graph.find_node(msg.node_id)
except KeyError:
logger.warning("Node message to unknown node:\n%s", msg)
return
assert isinstance(node, graph.ProcessorNode), type(node).__name__
proc = node.processor

12
noisicaa/audioproc/public/musical_time.pyx

@ -46,14 +46,14 @@ cdef class PyMusicalDuration(object):
if len(args) == 0:
self._duration = MusicalDuration()
elif len(args) == 2:
self._duration = MusicalDuration(<int>args[0], <int>args[1])
self._duration = MusicalDuration(<int64_t>args[0], <int64_t>args[1])
elif len(args) == 1:
if isinstance(args[0], PyMusicalDuration):
self._duration = (<PyMusicalDuration>args[0])._duration
elif isinstance(args[0], fractions.Fraction):
self._duration = MusicalDuration(<int>args[0].numerator, <int>args[0].denominator)
self._duration = MusicalDuration(<int64_t>args[0].numerator, <int64_t>args[0].denominator)
elif isinstance(args[0], int):
self._duration = MusicalDuration(<int>args[0])
self._duration = MusicalDuration(<int64_t>args[0])
else:
raise TypeError(repr(args[0]))
else:
@ -161,14 +161,14 @@ cdef class PyMusicalTime(object):
if len(args) == 0:
self._time = MusicalTime()
elif len(args) == 2:
self._time = MusicalTime(<int>args[0], <int>args[1])
self._time = MusicalTime(<int64_t>args[0], <int64_t>args[1])
elif len(args) == 1:
if isinstance(args[0], PyMusicalTime):
self._time = (<PyMusicalTime>args[0])._time
elif isinstance(args[0], fractions.Fraction):
self._time = MusicalTime(<int>args[0].numerator, <int>args[0].denominator)
self._time = MusicalTime(<int64_t>args[0].numerator, <int64_t>args[0].denominator)
elif isinstance(args[0], int):
self._time = MusicalTime(<int>args[0])
self._time = MusicalTime(<int64_t>args[0])
else:
raise TypeError(repr(args[0]))
else:

6
noisicaa/builtin_nodes/beat_track/track_ui.py

@ -124,6 +124,10 @@ class BeatMeasureEditor(measured_track_editor.MeasureEditor):
self.__ghost_time = None # type: audioproc.MusicalDuration
@property
def track_editor(self) -> 'BeatTrackEditor':
return down_cast(BeatTrackEditor, super().track_editor)
@property
def track(self) -> model.BeatTrack:
return down_cast(model.BeatTrack, super().track)
@ -238,7 +242,7 @@ class BeatTrackEditor(measured_track_editor.MeasuredTrackEditor):
self.__play_last_pitch = None # type: value_types.Pitch
self.setFixedHeight(60)
self.setDefaultHeight(60)
@property
def track(self) -> model.BeatTrack:

20
noisicaa/builtin_nodes/control_track/track_ui.py

@ -278,7 +278,7 @@ class ControlTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.
self.__listeners.add(self.track.points_changed.add(self.onPointsChanged))
self.setFixedHeight(120)
self.setDefaultHeight(120)
self.scaleXChanged.connect(self.__onScaleXChanged)
self.playbackPositionChanged.connect(self.__playbackPositionChanged)
@ -390,23 +390,7 @@ class ControlTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.
super().mouseDoubleClickEvent(evt)
def _paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
painter.setPen(Qt.black)
beat_time = audioproc.MusicalTime()
beat_num = 0
while beat_time < self.projectEndTime():
x = self.timeToX(beat_time)
if beat_num == 0:
painter.fillRect(x, 0, 2, self.height(), Qt.black)
else:
painter.fillRect(x, 0, 1, self.height(), QtGui.QColor(160, 160, 160))
beat_time += audioproc.MusicalDuration(1, 4)
beat_num += 1
x = self.timeToX(self.projectEndTime())
painter.fillRect(x, 0, 2, self.height(), Qt.black)
self.renderTimeGrid(painter, paint_rect)
points = self.points[:]

93
noisicaa/builtin_nodes/pianoroll_track/track_ui.py

@ -609,6 +609,8 @@ class SegmentEditor(
self.yOffsetChanged.connect(self.__grid.setYOffset)
self.__grid.setGridXSize(self.scaleX())
self.scaleXChanged.connect(self.__grid.setGridXSize)
self.__grid.setGridYSize(self.gridYSize())
self.gridYSizeChanged.connect(self.__grid.setGridYSize)
self.__grid.setReadOnly(self.readOnly())
self.readOnlyChanged.connect(self.__grid.setReadOnly)
self.__grid.setEditMode(self.editMode())
@ -634,8 +636,6 @@ class SegmentEditor(
self.__obj_to_grid_map[event.id] = event_id
self.__listeners.add(self.__segment.events_changed.add(self.__eventsChanged))
self.gridYSizeChanged.connect(self.__gridYSizeChanged)
def __selectedChanged(self, selected: bool) -> None:
if selected:
self.__grid.setOverlayColor(QtGui.QColor(150, 150, 255, 150))
@ -700,9 +700,6 @@ class SegmentEditor(
finally:
self.__ignore_model_mutations = False
def __gridYSizeChanged(self, size: int) -> None:
self.__grid.setGridYSize(size)
def segmentRef(self) -> model.PianoRollSegmentRef:
return self.__segment_ref
@ -743,10 +740,14 @@ class PianoRollTrackEditor(
base_track_editor.BaseTrackEditor):
yOffset, setYOffset, yOffsetChanged = slots.slot(int, 'yOffset', default=0)
gridYSize, setGridYSize, gridYSizeChanged = slots.slot(int, 'gridYSize', default=15)
effectiveGridYSize, setEffectiveGridYSize, effectiveGridYSizeChanged = slots.slot(
int, 'effectiveGridYSize', default=15)
hoverPitch, setHoverPitch, hoverPitchChanged = slots.slot(int, 'hoverPitch', default=-1)
snapToGrid, setSnapToGrid, snapToGridChanged = slots.slot(bool, 'snapToGrid', default=True)
currentChannel, setCurrentChannel, currentChannelChanged = slots.slot(
int, 'currentChannel', default=0)
showKeys, setShowKeys, showKeysChanged = slots.slot(
bool, 'showVelocity', default=False)
showVelocity, setShowVelocity, showVelocityChanged = slots.slot(
bool, 'showVelocity', default=False)
insertTime, setInsertTime, insertTimeChanged = slots.slot(
@ -773,16 +774,18 @@ class PianoRollTrackEditor(
self.__hover_pitch = -1
self.__keys = pianoroll.PianoKeys(parent=self)
self.__keys.setVisible(self.showKeys())
self.showKeysChanged.connect(self.__keys.setVisible)
self.__keys.setPlayable(True)
self.__keys.setPlaybackChannel(self.currentChannel())
self.currentChannelChanged.connect(self.__keys.setPlaybackChannel)
self.__keys.playNotes.connect(self.playNotes)
self.__keys.setScrollable(True)
self.__keys.setGridYSize(self.effectiveGridYSize())
self.effectiveGridYSizeChanged.connect(self.__keys.setGridYSize)
self.__keys.setYOffset(self.yOffset())
self.__keys.yOffsetChanged.connect(self.setYOffset)
self.yOffsetChanged.connect(self.__keys.setYOffset)
self.__keys.setGridYSize(self.gridYSize())
self.gridYSizeChanged.connect(self.__keys.setGridYSize)
self.hoverPitchChanged.connect(self.__hoverPitchChanged)
self.__y_scrollbar = QtWidgets.QScrollBar(orientation=Qt.Vertical, parent=self)
@ -826,16 +829,23 @@ class PianoRollTrackEditor(
self.__listeners.add(self.track.segments_changed.add(self.__segmentsChanged))
self.setAutoScroll(False)
self.setFixedHeight(240)
self.setDefaultHeight(240)
self.isCurrentChanged.connect(self.__isCurrentChanged)
self.isCurrentChanged.connect(lambda _: self.__updateShowKeys())
self.xOffsetChanged.connect(lambda _: self.__repositionSegments())
self.xOffsetChanged.connect(lambda _: self.update())
self.scaleXChanged.connect(lambda _: self.__repositionSegments())
self.gridYSizeChanged.connect(lambda _: self.__updateYScrollbar())
self.effectiveGridYSizeChanged.connect(lambda _: self.__updateYScrollbar())
self.effectiveGridYSizeChanged.connect(lambda _: self.__updateShowKeys())
self.playbackPositionChanged.connect(lambda _: self.updatePlaybackPosition())
self.insertTimeChanged.connect(lambda _: self.updateInsertTime())
self.xOffsetChanged.connect(lambda _: self.updateInsertTime())
self.scaleXChanged.connect(lambda _: self.updateInsertTime())
self.gridYSizeChanged.connect(lambda _: self.__updateEffectiveGridSize())
self.zoomChanged.connect(lambda _: self.__updateEffectiveGridSize())
self.__updateEffectiveGridSize()
self.__updateShowKeys()
self.setCurrentChannel(
self.get_session_value(self.__session_prefix + 'current-channel', 0))
@ -908,8 +918,8 @@ class PianoRollTrackEditor(
self.scaleXChanged.connect(seditor.setScaleX)
seditor.setYOffset(self.yOffset())
self.yOffsetChanged.connect(seditor.setYOffset)
seditor.setGridYSize(self.gridYSize())
self.gridYSizeChanged.connect(seditor.setGridYSize)
seditor.setGridYSize(self.effectiveGridYSize())
self.effectiveGridYSizeChanged.connect(seditor.setGridYSize)
seditor.setCurrentChannel(self.currentChannel())
self.currentChannelChanged.connect(seditor.setCurrentChannel)
seditor.setInsertVelocity(self.__velocity.value())
@ -955,8 +965,7 @@ class PianoRollTrackEditor(
if self.__hover_pitch >= 0:
self.__keys.noteOn(self.__hover_pitch)
def setIsCurrent(self, is_current: bool) -> None:
super().setIsCurrent(is_current)
def __isCurrentChanged(self, is_current: bool) -> None:
for segment in self.segments:
segment.setEnabled(is_current)
@ -1100,14 +1109,20 @@ class PianoRollTrackEditor(
self.__insert_cursor.setGeometry(x, 0, 3, self.height())
self.__insert_cursor.show()
def gridStep(self) -> audioproc.MusicalDuration:
for s in (64, 32, 16, 8, 4, 2):
if self.scaleX() / s > 96:
return audioproc.MusicalDuration(1, s)
return audioproc.MusicalDuration(1, 1)
def gridHeight(self) -> int:
return 128 * self.gridYSize() + 1
return 128 * self.effectiveGridYSize() + 1
def __updateEffectiveGridSize(self) -> None:
grid_y_size = max(1, int(self.zoom() * self.gridYSize()))
pos = (self.yOffset() + self.height() / 2) / self.gridHeight()
self.setEffectiveGridYSize(grid_y_size)
self.setYOffset(
max(0, min(self.gridHeight() - self.height(),
int(pos * self.gridHeight() - self.height() / 2))))
def __updateShowKeys(self) -> None:
self.setShowKeys(self.isCurrent() and self.effectiveGridYSize() > 3)
def repositionSegment(
self,
@ -1166,6 +1181,11 @@ class PianoRollTrackEditor(
self.__velocity_group.move(self.__keys.width(), 0)
pos = self.yOffset() + evt.oldSize().height() // 2
self.setYOffset(max(0, min(
max(0, self.gridHeight() - self.height()),
pos - evt.size().height() // 2)))
self.__y_scrollbar.move(self.width() - self.__y_scrollbar.width(), 0)
self.__y_scrollbar.resize(self.__y_scrollbar.width(), self.height())
@ -1193,9 +1213,9 @@ class PianoRollTrackEditor(
if evt.modifiers() == Qt.NoModifier:
offset = self.yOffset()
if evt.angleDelta().y() > 0:
offset -= 3 * self.gridYSize()
offset -= 3 * self.effectiveGridYSize()
elif evt.angleDelta().y() < 0:
offset += 3 * self.gridYSize()
offset += 3 * self.effectiveGridYSize()
offset = min(self.gridHeight() - self.height(), offset)
offset = max(0, offset)
if offset != self.yOffset():
@ -1206,34 +1226,7 @@ class PianoRollTrackEditor(
super().wheelEvent(evt)
def _paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
painter.setPen(Qt.black)
beat_time = audioproc.MusicalTime()
beat_num = 0
while beat_time < self.projectEndTime():
x = self.timeToX(beat_time)
if beat_num == 0:
painter.fillRect(x, 0, 2, self.height(), Qt.black)
else:
if beat_time % audioproc.MusicalTime(1, 4) == audioproc.MusicalTime(0, 1):
c = QtGui.QColor(160, 160, 160)
elif beat_time % audioproc.MusicalTime(1, 8) == audioproc.MusicalTime(0, 1):
c = QtGui.QColor(185, 185, 185)
elif beat_time % audioproc.MusicalTime(1, 16) == audioproc.MusicalTime(0, 1):
c = QtGui.QColor(210, 210, 210)
elif beat_time % audioproc.MusicalTime(1, 32) == audioproc.MusicalTime(0, 1):
c = QtGui.QColor(225, 225, 225)
else:
c = QtGui.QColor(240, 240, 240)
painter.fillRect(x, 0, 1, self.height(), c)
beat_time += self.gridStep()
beat_num += 1
x = self.timeToX(self.projectEndTime())
painter.fillRect(x, 0, 2, self.height(), Qt.black)
self.renderTimeGrid(painter, paint_rect)
def playNotes(self, play_notes: pianoroll.PlayNotes) -> None:
if self.playerState().playerID():

2
noisicaa/builtin_nodes/sample_track/track_ui.py

@ -301,7 +301,7 @@ class SampleTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.B
self.playbackPositionChanged.connect(self.__playbackPositionChanged)
self.setFixedHeight(120)
self.setDefaultHeight(120)
@property
def track(self) -> model.SampleTrack:

6
noisicaa/builtin_nodes/score_track/track_ui.py

@ -682,6 +682,10 @@ class ScoreMeasureEditor(measured_track_editor.MeasureEditor):
self.track_editor.currentToolChanged.connect(
lambda _: self.updateGhost(self.__mouse_pos))
@property
def track_editor(self) -> 'ScoreTrackEditor':
return down_cast(ScoreTrackEditor, super().track_editor)
@property
def track(self) -> model.ScoreTrack:
return down_cast(model.ScoreTrack, super().track)
@ -1126,7 +1130,7 @@ class ScoreTrackEditor(measured_track_editor.MeasuredTrackEditor):
super().__init__(**kwargs)
self.__play_last_pitch = None # type: value_types.Pitch
self.setFixedHeight(240)
self.setDefaultHeight(240)
def createToolBox(self) -> ScoreToolBox:
return ScoreToolBox(track=self, context=self.context)

12
noisicaa/builtin_nodes/step_sequencer/node_ui.py

@ -395,7 +395,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelMinValueChanged(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QLineEdit,
change: music.PropertyValueChange[float]
) -> None:
widget.setText(fmt_value(channel.min_value))
@ -403,7 +403,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelMinValueEdited(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QLineEdit
) -> None:
state, _, _ = widget.validator().validate(widget.text(), 0)
if state == QtGui.QValidator.Acceptable:
@ -416,7 +416,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelMaxValueChanged(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QLineEdit,
change: music.PropertyValueChange[float]
) -> None:
widget.setText(fmt_value(channel.max_value))
@ -424,7 +424,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelMaxValueEdited(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QLineEdit
) -> None:
state, _, _ = widget.validator().validate(widget.text(), 0)
if state == QtGui.QValidator.Acceptable:
@ -437,7 +437,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelLogScaleChanged(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QCheckBox,
change: music.PropertyValueChange[bool]
) -> None:
widget.setChecked(channel.log_scale)
@ -445,7 +445,7 @@ class StepSequencerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWid
def __channelLogScaleEdited(
self,
channel: model.StepSequencerChannel,
widget: control_value_dial.ControlValueDial,
widget: QtWidgets.QCheckBox,
value: bool
) -> None:
if value != channel.log_scale:

230
noisicaa/core/process_manager.py

@ -24,7 +24,6 @@ import asyncio
import base64
import concurrent.futures
import enum
import errno
import functools
import importlib
import logging
@ -47,10 +46,9 @@ import eventfd
from . import ipc
from . import stats
from . import stacktrace
from . import empty_message_pb2
from . import process_manager_pb2
from .logging import init_pylogging
from . import process_manager_io
if typing.TYPE_CHECKING:
import resource
@ -126,138 +124,6 @@ class LogAdapter(asyncio.Protocol):
self._state = 'header'
class ChildLogHandler(logging.Handler):
def __init__(self, log_fd: int) -> None:
super().__init__()
self.__log_fd = log_fd
self.__lock = threading.Lock()
def handle(self, record: logging.LogRecord) -> None:
record_attrs = {
'msg': record.getMessage(),
'args': (),
}
for attr in (
'created', 'exc_text', 'filename',
'funcName', 'levelname', 'levelno', 'lineno',
'module', 'msecs', 'name', 'pathname', 'process',
'relativeCreated', 'thread', 'threadName'):
record_attrs[attr] = record.__dict__[attr]
serialized_record = pickle.dumps(record_attrs, protocol=pickle.HIGHEST_PROTOCOL)
msg = bytearray()
msg += b'RECORD'
msg += struct.pack('>L', len(serialized_record))
msg += serialized_record
with self.__lock:
while msg:
written = os.write(self.__log_fd, msg)
msg = msg[written:]
def emit(self, record: logging.LogRecord) -> None:
pass
class ChildConnection(object):
def __init__(self, fd_in: int, fd_out: int) -> None:
self.fd_in = fd_in
self.fd_out = fd_out
self.__reader_state = 0
self.__reader_buf = None # type: bytearray
self.__reader_length = None # type: int
def write(self, request: bytes) -> None:
header = b'#%d\n' % len(request)
msg = header + request
while msg:
written = os.write(self.fd_out, msg)
msg = msg[written:]
def __reader_start(self) -> None:
self.__reader_state = 0
self.__reader_buf = None
self.__reader_length = None
def __read_internal(self) -> None:
if self.__reader_state == 0:
d = os.read(self.fd_in, 1)
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
assert d == b'#', d
self.__reader_buf = bytearray()
self.__reader_state = 1
elif self.__reader_state == 1:
d = os.read(self.fd_in, 1)
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
elif d == b'\n':
self.__reader_length = int(self.__reader_buf)
self.__reader_buf = bytearray()
self.__reader_state = 2
else:
self.__reader_buf += d
elif self.__reader_state == 2:
if len(self.__reader_buf) < self.__reader_length:
d = os.read(self.fd_in, self.__reader_length - len(self.__reader_buf))
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
self.__reader_buf += d
if len(self.__reader_buf) == self.__reader_length:
self.__reader_state = 3
@property
def __reader_done(self) -> bool:
return self.__reader_state == 3
@property
def __reader_response(self) -> bytes:
assert self.__reader_done
return self.__reader_buf
def read(self) -> bytes:
self.__reader_start()
while not self.__reader_done:
self.__read_internal()
return self.__reader_response
async def read_async(self, event_loop: asyncio.AbstractEventLoop) -> bytes:
done = asyncio.Event(loop=event_loop)
def read_cb() -> None:
try:
self.__read_internal()
except OSError:
event_loop.remove_reader(self.fd_in)
done.set()
return
except:
event_loop.remove_reader(self.fd_in)
raise
if self.__reader_done:
event_loop.remove_reader(self.fd_in)
done.set()
self.__reader_start()
event_loop.add_reader(self.fd_in, read_cb)
await done.wait()
if self.__reader_done:
return self.__reader_response
else:
raise OSError("Failed to read from connection")
def close(self) -> None:
os.close(self.fd_in)
os.close(self.fd_out)
class ChildCollector(object):
def __init__(self, stats_collector: stats.Collector, collection_interval: int = 100) -> None:
self.__stats_collector = stats_collector
@ -267,7 +133,7 @@ class ChildCollector(object):
self.__stat_poll_count = None # type: stats.Counter
self.__lock = threading.Lock()
self.__connections = {} # type: Dict[int, ChildConnection]
self.__connections = {} # type: Dict[int, process_manager_io.ChildConnection]
self.__stop = None # type: threading.Event
self.__thread = None # type: threading.Thread
@ -302,7 +168,7 @@ class ChildCollector(object):
self.__stat_poll_count.unregister()
self.__stat_poll_count = None
def add_child(self, pid: int, connection: ChildConnection) -> None:
def add_child(self, pid: int, connection: process_manager_io.ChildConnection) -> None:
with self.__lock:
self.__connections[pid] = connection
@ -316,7 +182,7 @@ class ChildCollector(object):
with self.__lock:
poll_start = time.perf_counter()
pending = {} # type: Dict[int, Tuple[float, int, ChildConnection]]
pending = {} # type: Dict[int, Tuple[float, int, process_manager_io.ChildConnection]]
poller = select.poll()
for pid, connection in self.__connections.items():
t0 = time.perf_counter()
@ -550,7 +416,7 @@ class ProcessManager(object):
# TODO: ensure that sys.stdout/err use utf-8
child_connection = ChildConnection(request_in, response_out)
child_connection = process_manager_io.ChildConnection(request_in, response_out)
# Wait until manager told us it's ok to start. Avoid race
# condition where child terminates and generates SIGCHLD
@ -573,7 +439,7 @@ class ProcessManager(object):
cmdline = [] # type: List[str]
cmdline += [sys.executable]
cmdline += ['-m', 'noisicaa.core.process_manager']
cmdline += ['-m', 'noisicaa.core.process_manager_entry']
cmdline += [base64.b64encode(
pickle.dumps(args, protocol=pickle.HIGHEST_PROTOCOL)).decode('ascii')]
@ -615,7 +481,7 @@ class ProcessManager(object):
proc.pid = pid
self._processes.add(proc)
child_connection = ChildConnection(response_in, request_out)
child_connection = process_manager_io.ChildConnection(response_in, request_out)
proc.create_loggers()
@ -688,7 +554,7 @@ class ProcessManager(object):
class ChildConnectionHandler(object):
def __init__(self, connection: ChildConnection) -> None:
def __init__(self, connection: process_manager_io.ChildConnection) -> None:
self.connection = connection
self.__stop = None # type: eventfd.EventFD
@ -811,69 +677,6 @@ class SubprocessMixin(ProcessBase):
self.pid = os.getpid()
self.executor = None # type: concurrent.futures.ThreadPoolExecutor
@staticmethod
def entry(argv: List[str]) -> None:
try:
assert len(argv) == 2
args = pickle.loads(base64.b64decode(argv[1]))
request_in = args['request_in']
response_out = args['response_out']
logger_out = args['logger_out']
log_level = args['log_level']
entry = args['entry']
name = args['name']
manager_address = args['manager_address']
tmp_dir = args['tmp_dir']
kwargs = args['kwargs']
# Remove all existing log handlers, and install a new
# handler to pipe all log messages back to the manager
# process.
root_logger = logging.getLogger()
while root_logger.handlers:
root_logger.removeHandler(root_logger.handlers[0])
root_logger.addHandler(ChildLogHandler(logger_out))
root_logger.setLevel(log_level)
# Make loggers of 3rd party modules less noisy.
for other in ['quamash']:
logging.getLogger(other).setLevel(logging.WARNING)
stacktrace.init()
init_pylogging()
mod_name, cls_name = entry.rsplit('.', 1)
mod = importlib.import_module(mod_name)
cls = getattr(mod, cls_name)
impl = cls(
name=name, manager_address=manager_address, tmp_dir=tmp_dir,
**kwargs)
child_connection = ChildConnection(request_in, response_out)
rc = impl.main(child_connection)
frames = sys._current_frames() # pylint: disable=protected-access
for thread in threading.enumerate():
if thread.ident == threading.get_ident():
continue
logger.warning("Left over thread %s (%x)", thread.name, thread.ident)
if thread.ident in frames:
logger.warning("".join(traceback.format_stack(frames[thread.ident])))
except SystemExit as exc:
rc = exc.code
except: # pylint: disable=bare-except
traceback.print_exc()
rc = 1
finally:
rc = rc or 0
sys.stdout.write("_exit(%d)\n" % rc)
sys.stdout.flush()
sys.stderr.flush()
os._exit(rc) # pylint: disable=protected-access
def create_event_loop(self) -> asyncio.AbstractEventLoop:
return asyncio.new_event_loop()
@ -901,7 +704,11 @@ class SubprocessMixin(ProcessBase):
sys.stderr.flush()
os._exit(1) # pylint: disable=protected-access
def main(self, child_connection: ChildConnection, *args: Any, **kwargs: Any) -> int:
def main(
self,
child_connection: process_manager_io.ChildConnection,
*args: Any, **kwargs: Any
) -> int:
event_loop_policy = EventLoopPolicy()
asyncio.set_event_loop_policy(event_loop_policy)
@ -936,7 +743,11 @@ class SubprocessMixin(ProcessBase):
self.event_loop.close()
logger.info("Event loop closed.")
async def main_async(self, child_connection: ChildConnection, *args: Any, **kwargs: Any) -> int:
async def main_async(
self,
child_connection: process_manager_io.ChildConnection,
*args: Any, **kwargs: Any
) -> int:
self.manager = ManagerStub(self.event_loop, self.manager_address)
async with self.manager:
try:
@ -1246,8 +1057,3 @@ class SubprocessHandle(ProcessHandle):
class ManagerStub(ipc.Stub):
pass
if __name__ == '__main__':
# Entry point for subprocesses.
SubprocessMixin.entry(sys.argv)

133
noisicaa/core/process_manager_entry.py

@ -0,0 +1,133 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import base64
import importlib
import logging
import os
import pickle
import struct
import sys
import threading
import traceback
from . import stacktrace
from .logging import init_pylogging
from . import process_manager_io
logger = logging.getLogger(__name__)
class ChildLogHandler(logging.Handler):
def __init__(self, log_fd: int) -> None:
super().__init__()
self.__log_fd = log_fd
self.__lock = threading.Lock()
def handle(self, record: logging.LogRecord) -> None:
record_attrs = {
'msg': record.getMessage(),
'args': (),
}
for attr in (
'created', 'exc_text', 'filename',
'funcName', 'levelname', 'levelno', 'lineno',
'module', 'msecs', 'name', 'pathname', 'process',
'relativeCreated', 'thread', 'threadName'):
record_attrs[attr] = record.__dict__[attr]
serialized_record = pickle.dumps(record_attrs, protocol=pickle.HIGHEST_PROTOCOL)
msg = bytearray()
msg += b'RECORD'
msg += struct.pack('>L', len(serialized_record))
msg += serialized_record
with self.__lock:
while msg:
written = os.write(self.__log_fd, msg)
msg = msg[written:]
def emit(self, record: logging.LogRecord) -> None:
pass
if __name__ == '__main__':
try:
assert len(sys.argv) == 2
args = pickle.loads(base64.b64decode(sys.argv[1]))
request_in = args['request_in']
response_out = args['response_out']
logger_out = args['logger_out']
log_level = args['log_level']
entry = args['entry']
name = args['name']
manager_address = args['manager_address']
tmp_dir = args['tmp_dir']
kwargs = args['kwargs']
# Remove all existing log handlers, and install a new
# handler to pipe all log messages back to the manager
# process.
root_logger = logging.getLogger()
while root_logger.handlers:
root_logger.removeHandler(root_logger.handlers[0])
root_logger.addHandler(ChildLogHandler(logger_out))
root_logger.setLevel(log_level)
# Make loggers of 3rd party modules less noisy.
for other in ['quamash']:
logging.getLogger(other).setLevel(logging.WARNING)
stacktrace.init()
init_pylogging()
mod_name, cls_name = entry.rsplit('.', 1)
mod = importlib.import_module(mod_name)
cls = getattr(mod, cls_name)
impl = cls(
name=name, manager_address=manager_address, tmp_dir=tmp_dir,
**kwargs)
child_connection = process_manager_io.ChildConnection(request_in, response_out)
rc = impl.main(child_connection)
frames = sys._current_frames() # pylint: disable=protected-access
for thread in threading.enumerate():
if thread.ident == threading.get_ident():
continue
logger.warning("Left over thread %s (%x)", thread.name, thread.ident)
if thread.ident in frames:
logger.warning("".join(traceback.format_stack(frames[thread.ident])))
except SystemExit as exc:
rc = exc.code
except: # pylint: disable=bare-except
traceback.print_exc()
rc = 1
finally:
rc = rc or 0
sys.stdout.write("_exit(%d)\n" % rc)
sys.stdout.flush()
sys.stderr.flush()
os._exit(rc) # pylint: disable=protected-access

127
noisicaa/core/process_manager_io.py

@ -0,0 +1,127 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import asyncio
import errno
import logging
import os
logger = logging.getLogger(__name__)
class ChildConnection(object):
def __init__(self, fd_in: int, fd_out: int) -> None:
self.fd_in = fd_in
self.fd_out = fd_out
self.__reader_state = 0
self.__reader_buf = None # type: bytearray
self.__reader_length = None # type: int
def write(self, request: bytes) -> None:
header = b'#%d\n' % len(request)
msg = header + request
while msg:
written = os.write(self.fd_out, msg)
msg = msg[written:]
def __reader_start(self) -> None:
self.__reader_state = 0
self.__reader_buf = None
self.__reader_length = None
def __read_internal(self) -> None:
if self.__reader_state == 0:
d = os.read(self.fd_in, 1)
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
assert d == b'#', d
self.__reader_buf = bytearray()
self.__reader_state = 1
elif self.__reader_state == 1:
d = os.read(self.fd_in, 1)
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
elif d == b'\n':
self.__reader_length = int(self.__reader_buf)
self.__reader_buf = bytearray()
self.__reader_state = 2
else:
self.__reader_buf += d
elif self.__reader_state == 2:
if len(self.__reader_buf) < self.__reader_length:
d = os.read(self.fd_in, self.__reader_length - len(self.__reader_buf))
if not d:
raise OSError(errno.EBADF, "File descriptor closed")
self.__reader_buf += d
if len(self.__reader_buf) == self.__reader_length:
self.__reader_state = 3
@property