Compare commits

...

39 Commits

Author SHA1 Message Date
Ben Niemann d456ba030d Initial high level tests for the editor app. 3 years ago
Ben Niemann fd1a958cb7 More tests for project_registry.py 3 years ago
Ben Niemann 2e7ab59e97 Use data from "Tome of Forbidden Spells" to generate project names. 3 years ago
Ben Niemann 4d022ce70b Minor UI tweaks. 3 years ago
Ben Niemann 10c30878d6 Fix lint issues and tests. 3 years ago
Ben Niemann 9b59f16060 Project list updates when projects get updated. Refuse to operate on opened projects. 3 years ago
Ben Niemann 65fbe84216 Make New/Open from menu work again and fix handling of multiple project tabs. 3 years ago
Ben Niemann 0a4d1994aa Remember currently opened projects and restore them on startup. 3 years ago
Ben Niemann 8d0ef9c2f0 Add a TODO for later. 3 years ago
Ben Niemann 5e2f83e25f Add a basic project debugger. 3 years ago
Ben Niemann 84f3d60454 Open project dialog persists state (search text, sorting) in settings. 3 years ago
Ben Niemann 35a16e9db5 Fix project list when projects are deleted. 3 years ago
Ben Niemann 69e0ab6b8a Gracefully handle exceptions when opening/creating projects. 3 years ago
Ben Niemann 1cbace1655 Fix some issues with the project list. 3 years ago
Ben Niemann dec25ac24c Change storage layout of projects. 3 years ago
Ben Niemann eba3e1abbf Implement FlatProjectListModel as an QAbstractProxyModel. 3 years ago
Ben Niemann 42bb081670 Random song title generator for project name suggestions. 3 years ago
Ben Niemann f1cb716f89 Make closing the current project work again. 3 years ago
Ben Niemann b3c17c14c3 Sort project list by name or mtime. 3 years ago
Ben Niemann 135e72a3ee Improve filtering the project list. 3 years ago
Ben Niemann b3e18c5bae Add a a comment for the future. 3 years ago
Ben Niemann 4c9d4a61de Sort and filter project list. 3 years ago
Ben Niemann b1383d8387 Do not allow creating project on existing path. 3 years ago
Ben Niemann b93807abb7 Implement deleting projects. 3 years ago
Ben Niemann 11f79fec31 Add a "New Project" button. 3 years ago
Ben Niemann b43eb88d73 Implement the ProjectRegistry as an QAbstractItemModel. 3 years ago
Ben Niemann 9b31dd54b8 Remove obsolete ProjectMixin.editor_window property. 3 years ago
Ben Niemann 107a8f5fa8 Closing the editor window calls deleteWindow() on the app. 3 years ago
Ben Niemann 3e71609b19 Do not create settings dialog on demand. 3 years ago
Ben Niemann 947af52e87 Separate out the model for the instrument list. 3 years ago
Ben Niemann a6e06da89e More more global (not project related) stuff to the app. 3 years ago
Ben Niemann 0a07ac9040 Instrument library is owned by app. 3 years ago
Ben Niemann 876d8f5066 Settings dialog is owned by app. 3 years ago
Ben Niemann 6d67e2a7c5 Use CommonMixin for project registry. 3 years ago
Ben Niemann 7e0999fe96 Initial version of reorganized app startup and custom project dialog. 3 years ago
Ben Niemann fcf1db67e6 Explicitly set the executor on the event loop. 3 years ago
Ben Niemann 7e06b1155a Use my own patched version of quamash. 3 years ago
Ben Niemann 7259fb1c40 Remove some cruft. 3 years ago
Ben Niemann 7982ec5934 Ignore session values for non-existing nodes. 3 years ago
  1. 54
      3rdparty/typeshed/PyQt5/QtCore.pyi
  2. 16
      3rdparty/typeshed/PyQt5/QtWidgets.pyi
  3. 2
      3rdparty/typeshed/humanize.pyi
  4. 4
      listdeps
  5. 1
      noisicaa/CMakeLists.txt
  6. 8
      noisicaa/audioproc/engine/realm.pyx
  7. 2
      noisicaa/builtin_nodes/custom_csound/node_ui.py
  8. 2
      noisicaa/builtin_nodes/instrument/node_ui.py
  9. 2
      noisicaa/builtin_nodes/sample_track/track_ui.py
  10. 28
      noisicaa/builtin_nodes/score_track/track_ui.py
  11. 8
      noisicaa/core/process_manager.py
  12. 56
      noisicaa/core/storage.py
  13. 20
      noisicaa/core/storage_test.py
  14. 7
      noisicaa/instrument_db/client.py
  15. 12
      noisicaa/music/project_test.py
  16. 4
      noisicaa/music/writer_process.py
  17. 25
      noisicaa/title_generator/CMakeLists.txt
  18. 21
      noisicaa/title_generator/__init__.py
  19. 1124
      noisicaa/title_generator/data.py
  20. 50
      noisicaa/title_generator/title_generator.py
  21. 30
      noisicaa/title_generator/title_generator_test.py
  22. 10
      noisicaa/ui/CMakeLists.txt
  23. 448
      noisicaa/ui/editor_app.py
  24. 222
      noisicaa/ui/editor_app_test.py
  25. 705
      noisicaa/ui/editor_window.py
  26. 2
      noisicaa/ui/graph/plugin_node.py
  27. 299
      noisicaa/ui/instrument_library.py
  28. 308
      noisicaa/ui/instrument_list.py
  29. 8
      noisicaa/ui/instrument_list_test.py
  30. 559
      noisicaa/ui/open_project_dialog.py
  31. 39
      noisicaa/ui/open_project_dialog_test.py
  32. 289
      noisicaa/ui/project_debugger.py
  33. 341
      noisicaa/ui/project_registry.py
  34. 127
      noisicaa/ui/project_registry_test.py
  35. 2
      noisicaa/ui/project_view.py
  36. 0
      noisicaa/ui/settings_dialog.py
  37. 1
      noisicaa/ui/track_list/view.py
  38. 60
      noisicaa/ui/ui_base.py
  39. 2
      noisidev/uitest.py

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

@ -1852,6 +1852,20 @@ class QPersistentModelIndex(sip.simplewrapper):
class QAbstractItemModel(QObject):
dataChanged = ... # type: PYQT_SIGNAL
columnsMoved = ... # type: PYQT_SIGNAL
columnsAboutToBeMoved = ... # type: PYQT_SIGNAL
rowsMoved = ... # type: PYQT_SIGNAL
rowsAboutToBeMoved = ... # type: PYQT_SIGNAL
modelReset = ... # type: PYQT_SIGNAL
modelAboutToBeReset = ... # type: PYQT_SIGNAL
columnsRemoved = ... # type: PYQT_SIGNAL
columnsAboutToBeRemoved = ... # type: PYQT_SIGNAL
columnsInserted = ... # type: PYQT_SIGNAL
columnsAboutToBeInserted = ... # type: PYQT_SIGNAL
rowsRemoved = ... # type: PYQT_SIGNAL
rowsAboutToBeRemoved = ... # type: PYQT_SIGNAL
rowsInserted = ... # type: PYQT_SIGNAL
rowsAboutToBeInserted = ... # type: PYQT_SIGNAL
class LayoutChangeHint(int): ...
NoLayoutChangeHint = ... # type: 'QAbstractItemModel.LayoutChangeHint'
@ -1872,10 +1886,10 @@ class QAbstractItemModel(QObject):
def beginMoveColumns(self, sourceParent: QModelIndex, sourceFirst: int, sourceLast: int, destinationParent: QModelIndex, destinationColumn: int) -> bool: ...
def endMoveRows(self) -> None: ...
def beginMoveRows(self, sourceParent: QModelIndex, sourceFirst: int, sourceLast: int, destinationParent: QModelIndex, destinationRow: int) -> bool: ...
def columnsMoved(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, column: int) -> None: ...
def columnsAboutToBeMoved(self, sourceParent: QModelIndex, sourceStart: int, sourceEnd: int, destinationParent: QModelIndex, destinationColumn: int) -> None: ...
def rowsMoved(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, row: int) -> None: ...
def rowsAboutToBeMoved(self, sourceParent: QModelIndex, sourceStart: int, sourceEnd: int, destinationParent: QModelIndex, destinationRow: int) -> None: ...
#def columnsMoved(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, column: int) -> None: ...
#def columnsAboutToBeMoved(self, sourceParent: QModelIndex, sourceStart: int, sourceEnd: int, destinationParent: QModelIndex, destinationColumn: int) -> None: ...
#def rowsMoved(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, row: int) -> None: ...
#def rowsAboutToBeMoved(self, sourceParent: QModelIndex, sourceStart: int, sourceEnd: int, destinationParent: QModelIndex, destinationRow: int) -> None: ...
def createIndex(self, row: int, column: int, object: typing.Any = ...) -> QModelIndex: ...
def roleNames(self) -> typing.Any: ...
def supportedDragActions(self) -> Qt.DropActions: ...
@ -1898,16 +1912,16 @@ class QAbstractItemModel(QObject):
def encodeData(self, indexes: typing.Any, stream: 'QDataStream') -> None: ...
def revert(self) -> None: ...
def submit(self) -> bool: ...
def modelReset(self) -> None: ...
def modelAboutToBeReset(self) -> None: ...
def columnsRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
def columnsAboutToBeRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
def columnsInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
def columnsAboutToBeInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
def rowsRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
def rowsAboutToBeRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
def rowsInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
def rowsAboutToBeInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def modelReset(self) -> None: ...
#def modelAboutToBeReset(self) -> None: ...
#def columnsRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def columnsAboutToBeRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def columnsInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def columnsAboutToBeInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def rowsRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def rowsAboutToBeRemoved(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def rowsInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
#def rowsAboutToBeInserted(self, parent: QModelIndex, first: int, last: int) -> None: ...
def layoutChanged(self, parents: typing.Iterable[QPersistentModelIndex] = ..., hint: 'QAbstractItemModel.LayoutChangeHint' = ...) -> None: ...
def layoutAboutToBeChanged(self, parents: typing.Any = ..., hint: 'QAbstractItemModel.LayoutChangeHint' = ...) -> None: ...
def headerDataChanged(self, orientation: Qt.Orientation, first: int, last: int) -> None: ...
@ -3941,6 +3955,10 @@ class QItemSelectionRange(sip.simplewrapper):
class QItemSelectionModel(QObject):
currentColumnChanged = ... # type: PYQT_SIGNAL
currentRowChanged = ... # type: PYQT_SIGNAL
currentChanged = ... # type: PYQT_SIGNAL
selectionChanged = ... # type: PYQT_SIGNAL
class SelectionFlag(int): ...
NoUpdate = ... # type: 'QItemSelectionModel.SelectionFlag'
@ -3980,10 +3998,10 @@ class QItemSelectionModel(QObject):
def selectedRows(self, column: int = ...) -> typing.Any: ...
def hasSelection(self) -> bool: ...
def emitSelectionChanged(self, newSelection: 'QItemSelection', oldSelection: 'QItemSelection') -> None: ...
def currentColumnChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
def currentRowChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
def currentChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
def selectionChanged(self, selected: 'QItemSelection', deselected: 'QItemSelection') -> None: ...
#def currentColumnChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
#def currentRowChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
#def currentChanged(self, current: QModelIndex, previous: QModelIndex) -> None: ...
#def selectionChanged(self, selected: 'QItemSelection', deselected: 'QItemSelection') -> None: ...
def clearCurrentIndex(self) -> None: ...
def setCurrentIndex(self, index: QModelIndex, command: typing.Union['QItemSelectionModel.SelectionFlags', 'QItemSelectionModel.SelectionFlag']) -> None: ...
@typing.overload

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

@ -567,6 +567,10 @@ class QAbstractScrollArea(QFrame):
class QAbstractItemView(QAbstractScrollArea):
activated = ... # type: PYQT_SIGNAL
doubleClicked = ... # type: PYQT_SIGNAL
clicked = ... # type: PYQT_SIGNAL
pressed = ... # type: PYQT_SIGNAL
class DropIndicatorPosition(int): ...
OnItem = ... # type: 'QAbstractItemView.DropIndicatorPosition'
@ -707,10 +711,10 @@ class QAbstractItemView(QAbstractScrollArea):
def iconSizeChanged(self, size: QtCore.QSize) -> None: ...
def viewportEntered(self) -> None: ...
def entered(self, index: QtCore.QModelIndex) -> None: ...
def activated(self, index: QtCore.QModelIndex) -> None: ...
def doubleClicked(self, index: QtCore.QModelIndex) -> None: ...
def clicked(self, index: QtCore.QModelIndex) -> None: ...
def pressed(self, index: QtCore.QModelIndex) -> None: ...
#def activated(self, index: QtCore.QModelIndex) -> None: ...
#def doubleClicked(self, index: QtCore.QModelIndex) -> None: ...
#def clicked(self, index: QtCore.QModelIndex) -> None: ...
#def pressed(self, index: QtCore.QModelIndex) -> None: ...
def editorDestroyed(self, editor: QtCore.QObject) -> None: ...
def commitData(self, editor: QWidget) -> None: ...
def closeEditor(self, editor: QWidget, hint: QAbstractItemDelegate.EndEditHint) -> None: ...
@ -2340,6 +2344,8 @@ class QStyle(QtCore.QObject):
def __invert__(self) -> 'QStyle.State': ...
def __int__(self) -> int: ...
def __and__(self, o: typing.Union[int, 'QStyle.State', 'QStyle.StateFlag']) -> 'QStyle.State': ...
class SubControls(sip.simplewrapper):
@typing.overload
@ -6436,6 +6442,8 @@ class QMessageBox(QDialog):
def __invert__(self) -> 'QMessageBox.StandardButtons': ...
def __int__(self) -> int: ...
def __ior__(self, o: typing.Union[int, 'QMessageBox.StandardButtons', 'QMessageBox.StandardButton']) -> 'QMessageBox.StandardButtons': ...
@typing.overload
def __init__(self, parent: typing.Optional[QWidget] = ...) -> None: ...
@typing.overload

2
3rdparty/typeshed/humanize.pyi vendored

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

4
listdeps

@ -54,6 +54,7 @@ PIP_DEPS = {
PKG('PyQt5'),
PKG('eventfd'),
PKG('gbulb'),
PKG('lucky-humanize'),
PKG('numpy'),
PKG('portalocker'),
PKG('posix_ipc'),
@ -61,7 +62,8 @@ PIP_DEPS = {
PKG('psutil'),
PKG('pyaudio'),
PKG('pyparsing'),
PKG('quamash'),
# TODO: get my changes upstream and use regular quamash package from pip.
PKG('git+https://github.com/odahoda/quamash.git#egg=quamash'),
PKG('toposort'),
PKG('urwid'),
],

1
noisicaa/CMakeLists.txt

@ -43,3 +43,4 @@ add_subdirectory(music)
add_subdirectory(node_db)
add_subdirectory(ui)
add_subdirectory(builtin_nodes)
add_subdirectory(title_generator)

8
noisicaa/audioproc/engine/realm.pyx

@ -231,8 +231,12 @@ cdef class PyRealm(object):
key = session_value.name
if key.startswith('node/'):
_, node_id, node_key = key.split('/', 2)
node = self.__graph.find_node(node_id)
node.set_session_value(node_key, session_value)
try:
node = self.__graph.find_node(node_id)
except KeyError:
pass
else:
node.set_session_value(node_key, session_value)
def send_node_message(self, msg):
node = self.__graph.find_node(msg.node_id)

2
noisicaa/builtin_nodes/custom_csound/node_ui.py

@ -451,7 +451,7 @@ class CustomCSoundNode(generic_node.GenericNode):
def __showEditor(self) -> None:
if self.__editor is None:
self.__editor = Editor(
node=self.__node, parent=self.editor_window, context=self.context)
node=self.__node, parent=self.project_view, context=self.context)
self.__editor.show()
self.__editor.raise_()

2
noisicaa/builtin_nodes/instrument/node_ui.py

@ -93,7 +93,7 @@ class InstrumentNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidget
async def __selectInstrument(self) -> None:
dialog = instrument_library.InstrumentLibraryDialog(
context=self.context, selectButton=True, parent=self.editor_window)
context=self.context, selectButton=True, parent=self.project_view)
dialog.setWindowTitle("Select instrument")
dialog.setModal(True)
dialog.finished.connect(lambda _: self.__selectInstrumentClosed(dialog))

2
noisicaa/builtin_nodes/sample_track/track_ui.py

@ -373,7 +373,7 @@ class SampleTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.B
def onAddSample(self, time: audioproc.MusicalTime) -> None:
path, _ = QtWidgets.QFileDialog.getOpenFileName(
parent=self.editor_window,
parent=self.project_view,
caption="Add Sample to track \"%s\"" % self.track.name,
#directory=self.ui_state.get(
#'instruments_add_dialog_path', ''),

28
noisicaa/builtin_nodes/score_track/track_ui.py

@ -69,17 +69,17 @@ class ScoreToolBase(measured_track_editor.MeasuredToolBase):
self._updateGhost(target, evt.pos())
ymid = target.height() // 2
stave_line = (
int(ymid + 5 - evt.pos().y()) // 10 + target.measure.clef.center_pitch.stave_line)
idx, _, _ = target.getEditArea(evt.pos().x())
if idx < 0:
self.editor_window.setInfoMessage('')
else:
pitch = value_types.Pitch.name_from_stave_line(
stave_line, target.measure.key_signature)
self.editor_window.setInfoMessage(pitch)
# ymid = target.height() // 2
# stave_line = (
# int(ymid + 5 - evt.pos().y()) // 10 + target.measure.clef.center_pitch.stave_line)
# idx, _, _ = target.getEditArea(evt.pos().x())
# if idx < 0:
# self.editor_window.setInfoMessage('')
# else:
# pitch = value_types.Pitch.name_from_stave_line(
# stave_line, target.measure.key_signature)
# self.editor_window.setInfoMessage(pitch)
super().mouseMoveEvent(target, evt)
@ -831,12 +831,6 @@ class ScoreMeasureEditor(measured_track_editor.MeasureEditor):
# if overflow:
# n.setOpacity(0.4)
# if self.app.showEditAreas:
# info = QtWidgets.QGraphicsSimpleTextItem(self)
# info.setText(
# '%d/%d' % (min_stave_line, max_stave_line))
# info.setPos(x - 10, 0)
x1 = max(x - 12, px)
x2 = max(x + 13, x1)
if x1 > px:

8
noisicaa/core/process_manager.py

@ -22,6 +22,7 @@
import asyncio
import base64
import concurrent.futures
import enum
import errno
import functools
@ -808,6 +809,7 @@ class SubprocessMixin(ProcessBase):
self.manager_address = manager_address
self.pid = os.getpid()
self.executor = None # type: concurrent.futures.ThreadPoolExecutor
@staticmethod
def entry(argv: List[str]) -> None:
@ -907,6 +909,9 @@ class SubprocessMixin(ProcessBase):
self.event_loop = self.create_event_loop()
self.event_loop.set_exception_handler(self.error_handler)
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
self.event_loop.set_default_executor(self.executor)
# mypy doesn't know about asyncio.SafeChildWatcher.
child_watcher = asyncio.SafeChildWatcher() # type: ignore
child_watcher.attach_loop(self.event_loop)
@ -916,6 +921,9 @@ class SubprocessMixin(ProcessBase):
return self.event_loop.run_until_complete(
self.main_async(child_connection, *args, **kwargs))
finally:
logger.info("Closing executor...")
self.executor.shutdown()
logger.info("Closing event loop...")
pending_tasks = asyncio.Task.all_tasks(self.event_loop)
if pending_tasks:

56
noisicaa/core/storage.py

@ -52,7 +52,7 @@ class CorruptedProjectError(Error):
pass
HeaderData = TypedDict('HeaderData', {'data_dir': str, 'created': int})
HeaderData = TypedDict('HeaderData', {'created': int})
LogEntry = bytes
Checkpoint = bytes
@ -81,7 +81,6 @@ class ProjectStorage(object):
def __init__(self) -> None:
self.path = None # type: str
self.data_dir = None # type: str
self.header_data = None # type: HeaderData
self.file_lock = None # type: IO
self.log_index_fp = None # type: IO[bytes]
@ -118,7 +117,7 @@ class ProjectStorage(object):
logger.info("Opening project at %s", self.path)
try:
fp = fileutil.File(path)
fp = fileutil.File(os.path.join(path, 'project.noise'))
file_info, self.header_data = fp.read_json()
except fileutil.Error as exc:
raise FileOpenError(str(exc))
@ -127,18 +126,13 @@ class ProjectStorage(object):
raise FileOpenError("Not a project file")
if file_info.version not in self.SUPPORTED_VERSIONS:
raise UnsupportedFileVersionError()
self.data_dir = os.path.join(
os.path.dirname(self.path), self.header_data['data_dir'])
if not os.path.isdir(self.data_dir):
raise CorruptedProjectError(
"Directory %s missing" % self.data_dir)
raise UnsupportedFileVersionError(
"File version %d not supported" % file_info.version)
self.file_lock = self.acquire_file_lock(
os.path.join(self.data_dir, "lock"))
os.path.join(self.path, "lock"))
log_path = os.path.join(self.data_dir, 'log.%06d' % self.log_file_number)
log_path = os.path.join(self.path, 'log.%06d' % self.log_file_number)
if os.path.exists(log_path):
mode = 'a+b'
else:
@ -146,7 +140,7 @@ class ProjectStorage(object):
self.log_fp = open(log_path, mode=mode, buffering=0)
self.log_index_fp = open(
os.path.join(self.data_dir, 'log.index'),
os.path.join(self.path, 'log.index'),
mode='r+b', buffering=0)
self.log_index = bytearray(self.log_index_fp.read())
self.next_log_number = len(self.log_index) // self.log_index_formatter.size
@ -155,7 +149,7 @@ class ProjectStorage(object):
self.written_log_number = self.next_log_number - 1
self.log_history_fp = open(
os.path.join(self.data_dir, 'log.history'),
os.path.join(self.path, 'log.history'),
mode='r+b', buffering=0)
self.log_history = bytearray(self.log_history_fp.read())
self.next_sequence_number = len(self.log_history) // self.log_history_formatter.size
@ -164,14 +158,14 @@ class ProjectStorage(object):
self.written_sequence_number = self.next_sequence_number - 1
if self.written_sequence_number >= 0:
self.undo_count, self.redo_count = self._get_history_entry(
self.undo_count, self.redo_count = self.get_history_entry(
self.written_sequence_number)[2:4]
else:
self.undo_count = 0
self.redo_count = 0
self.checkpoint_index_fp = open(
os.path.join(self.data_dir, 'checkpoint.index'),
os.path.join(self.path, 'checkpoint.index'),
mode='r+b', buffering=0)
self.checkpoint_index = bytearray(self.checkpoint_index_fp.read())
self.next_checkpoint_number = (
@ -180,6 +174,8 @@ class ProjectStorage(object):
self.next_checkpoint_number * self.checkpoint_index_formatter.size):
raise CorruptedProjectError("Malformed checkpoint.index file.")
os.utime(os.path.join(self.path, 'project.noise'))
def get_restore_info(self) -> Tuple[int, List[Tuple[Action, int]]]:
assert self.next_checkpoint_number > 0
@ -188,7 +184,7 @@ class ProjectStorage(object):
actions = []
for snum in range(seq_number, self.next_sequence_number):
action, log_number = self._get_history_entry(snum)[0:2]
action, log_number = self.get_history_entry(snum)[0:2]
actions.append((Action(action), log_number))
return checkpoint_number, actions
@ -197,17 +193,15 @@ class ProjectStorage(object):
def create(cls, path: str) -> 'ProjectStorage':
header_data = {
'created': int(time.time()),
'data_dir': os.path.splitext(os.path.basename(path))[0] + '.data',
} # type: HeaderData
data_dir = os.path.join(os.path.dirname(path), header_data['data_dir'])
os.mkdir(data_dir)
os.mkdir(path)
for fname in ('lock', 'log.index', 'log.history',
'checkpoint.index'):
open(os.path.join(data_dir, fname), 'wb').close()
open(os.path.join(path, fname), 'wb').close()
fp = fileutil.File(path)
fp = fileutil.File(os.path.join(path, 'project.noise'))
fp.write_json(
header_data,
fileutil.FileInfo(
@ -223,6 +217,8 @@ class ProjectStorage(object):
def close(self) -> None:
assert self.path is not None, "Project already closed."
os.utime(os.path.join(self.path, 'project.noise'))
self.path = None
if self.log_index_fp is not None:
@ -295,7 +291,7 @@ class ProjectStorage(object):
def _write_checkpoint(
self, seq_number: int, checkpoint_number: int, checkpoint: Checkpoint) -> None:
checkpoint_path = os.path.join(
self.data_dir,
self.path,
'checkpoint.%06d' % checkpoint_number)
logger.info("Writing checkpoint %s...", checkpoint_path)
with open(checkpoint_path, mode='wb', buffering=0) as fp:
@ -339,7 +335,7 @@ class ProjectStorage(object):
return header
def _get_history_entry(self, seq_number: int) -> HistoryEntry:
def get_history_entry(self, seq_number: int) -> HistoryEntry:
size = self.log_history_formatter.size
offset = seq_number * size
packed_entry = self.log_history[offset:offset+size]
@ -355,7 +351,7 @@ class ProjectStorage(object):
except KeyError:
log_fp = open(
os.path.join(
self.data_dir,
self.path,
'log.%06d' % file_number),
mode='r+b', buffering=0)
self.log_fp_map[self.log_file_number] = log_fp
@ -440,7 +436,7 @@ class ProjectStorage(object):
entry_to_undo = self.next_sequence_number - 2 * self.undo_count - 1
action, log_number = self._get_history_entry(entry_to_undo)[0:2]
action, log_number = self.get_history_entry(entry_to_undo)[0:2]
return Action(_reverse_action(action)), self.get_log_entry(log_number)
def get_log_entry_to_redo(self) -> Tuple[Action, LogEntry]:
@ -448,7 +444,7 @@ class ProjectStorage(object):
entry_to_redo = self.next_sequence_number - 2 * self.undo_count
action, log_number = self._get_history_entry(entry_to_redo)[0:2]
action, log_number = self.get_history_entry(entry_to_redo)[0:2]
return Action(action), self.get_log_entry(log_number)
def undo(self) -> None:
@ -456,7 +452,7 @@ class ProjectStorage(object):
assert self.can_undo
entry_to_undo = self.next_sequence_number - 2 * self.undo_count - 1
action, log_number = self._get_history_entry(entry_to_undo)[0:2]
action, log_number = self.get_history_entry(entry_to_undo)[0:2]
self.undo_count += 1
history_entry = (_reverse_action(action), log_number, self.undo_count, self.redo_count)
@ -471,7 +467,7 @@ class ProjectStorage(object):
assert self.can_redo
entry_to_redo = self.next_sequence_number - 2 * self.undo_count
action, log_number = self._get_history_entry(entry_to_redo)[0:2]
action, log_number = self.get_history_entry(entry_to_redo)[0:2]
self.redo_count += 1
history_entry = (action, log_number, self.undo_count, self.redo_count)
@ -501,7 +497,7 @@ class ProjectStorage(object):
checkpoint_number = self._get_checkpoint_entry(checkpoint_number)[1]
checkpoint_path = os.path.join(
self.data_dir,
self.path,
'checkpoint.%06d' % checkpoint_number)
logger.info("Reading checkpoint %s...", checkpoint_path)
with open(checkpoint_path, mode='rb') as fp:

20
noisicaa/core/storage_test.py

@ -123,17 +123,19 @@ class StorageTest(unittest.TestCase):
ps.close()
self.assertTrue(
self.fake_os.path.isfile('/foo.data/log.index'))
self.fake_os.path.isfile('/foo/project.noise'))
self.assertTrue(
self.fake_os.path.isfile('/foo/log.index'))
self.assertEqual(
self.fake_os.path.getsize('/foo.data/log.index'),
self.fake_os.path.getsize('/foo/log.index'),
3 * ps.log_index_formatter.size)
self.assertTrue(
self.fake_os.path.isfile('/foo.data/log.history'))
self.fake_os.path.isfile('/foo/log.history'))
self.assertEqual(
self.fake_os.path.getsize('/foo.data/log.history'),
self.fake_os.path.getsize('/foo/log.history'),
7 * ps.log_history_formatter.size)
self.assertTrue(
self.fake_os.path.isfile('/foo.data/log.000000'))
self.fake_os.path.isfile('/foo/log.000000'))
def test_undo_the_undone(self):
ps = storage.ProjectStorage.create('/foo')
@ -235,11 +237,11 @@ class StorageTest(unittest.TestCase):
ps.close()
self.assertTrue(
self.fake_os.path.isfile('/foo.data/checkpoint.index'))
self.fake_os.path.isfile('/foo/checkpoint.index'))
self.assertEqual(
self.fake_os.path.getsize('/foo.data/checkpoint.index'),
self.fake_os.path.getsize('/foo/checkpoint.index'),
2 * ps.checkpoint_index_formatter.size)
self.assertTrue(
self.fake_os.path.isfile('/foo.data/checkpoint.000000'))
self.fake_os.path.isfile('/foo/checkpoint.000000'))
self.assertTrue(
self.fake_os.path.isfile('/foo.data/checkpoint.000001'))
self.fake_os.path.isfile('/foo/checkpoint.000001'))

7
noisicaa/instrument_db/client.py

@ -82,16 +82,17 @@ class InstrumentDBClient(object):
async def start_scan(self) -> None:
await self.__stub.call('START_SCAN')
def __handle_mutation(
async def __handle_mutation(
self,
request: instrument_db_pb2.Mutations,
response: empty_message_pb2.EmptyMessage,
) -> None:
for mutation in request.mutations:
logger.info("Mutation received: %s", mutation)
for idx, mutation in enumerate(request.mutations):
if mutation.WhichOneof('type') == 'add_instrument':
self.__instruments[mutation.add_instrument.uri] = mutation.add_instrument
else:
raise ValueError(mutation)
self.mutation_handlers.call(mutation)
if idx % 10 == 0:
await asyncio.sleep(0, loop=self.event_loop)

12
noisicaa/music/project_test.py

@ -112,16 +112,16 @@ class ProjectTest(
async def test_create(self):
p = await project.Project.create_blank(
path='/foo.noise',
path='/foo',
pool=self.pool,
writer=self.writer_client,
node_db=self.node_db)
await p.close()
self.assertTrue(self.fake_os.path.isfile('/foo.noise'))
self.assertTrue(self.fake_os.path.isdir('/foo.data'))
self.assertTrue(self.fake_os.path.isdir('/foo'))
self.assertTrue(self.fake_os.path.isfile('/foo/project.noise'))
f = fileutil.File('/foo.noise')
f = fileutil.File('/foo/project.noise')
file_info, contents = f.read_json()
self.assertEqual(file_info.version, 1)
self.assertEqual(file_info.filetype, 'project-header')
@ -155,7 +155,7 @@ class ProjectTest(
async def test_create_checkpoint(self):
p = await project.Project.create_blank(
path='/foo.noise',
path='/foo',
pool=self.pool,
writer=self.writer_client,
node_db=self.node_db)
@ -165,7 +165,7 @@ class ProjectTest(
await p.close()
self.assertTrue(
self.fake_os.path.isfile('/foo.data/checkpoint.000001'))
self.fake_os.path.isfile('/foo/checkpoint.000001'))
async def test_merge_mutations(self):
p = await project.Project.create_blank(

4
noisicaa/music/writer_process.py

@ -86,7 +86,7 @@ class WriterProcess(core.ProcessBase):
assert self.__storage is None
self.__storage = storage.ProjectStorage.create(request.path)
response.data_dir = self.__storage.data_dir
response.data_dir = self.__storage.path
self.__storage.add_checkpoint(request.initial_checkpoint)
response.storage_state.CopyFrom(self.__get_storage_state())
@ -100,7 +100,7 @@ class WriterProcess(core.ProcessBase):
self.__storage = storage.ProjectStorage()
self.__storage.open(request.path)
response.data_dir = self.__storage.data_dir
response.data_dir = self.__storage.path
checkpoint_number, actions = self.__storage.get_restore_info()

25
noisicaa/title_generator/CMakeLists.txt

@ -0,0 +1,25 @@
# @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
add_python_package(
data.py
title_generator.py
title_generator_test.py
)

21
noisicaa/title_generator/__init__.py

@ -0,0 +1,21 @@
# @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
from .title_generator import TitleGenerator

1124
noisicaa/title_generator/data.py

File diff suppressed because it is too large Load Diff

50
noisicaa/title_generator/title_generator.py

@ -0,0 +1,50 @@
# @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 random
import re
from typing import List
from . import data
class TitleGenerator(object):
def __init__(self, seed: int = None) -> None:
self.__rnd = random.Random(seed)
def __pick(self, lst: List[str]) -> str:
return self.__rnd.choice(lst)
def generate(self) -> str:
sentence = self.__pick(data.sentence)
while True:
n_sentence = re.sub(
r'\${([^}]+)}',
lambda m: self.__pick(data.word_sets[m.group(1)]),
sentence)
if n_sentence == sentence:
break
sentence = n_sentence
sentence = sentence.replace("**", "The ")
sentence = sentence.replace("*", "")
return sentence

30
noisicaa/title_generator/title_generator_test.py

@ -0,0 +1,30 @@
#!/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
from noisidev import unittest
from . import title_generator
class TitleGeneratorTest(unittest.TestCase):
def test_generate(self):
gen = title_generator.TitleGenerator()
for _ in range(20000):
gen.generate()

10
noisicaa/ui/CMakeLists.txt

@ -25,22 +25,28 @@ add_python_package(
device_list.py
dynamic_layout.py
editor_app.py
editor_app_test.py
editor_window.py
flowlayout.py
gain_slider.py
instrument_library.py
instrument_library_test.py
instrument_list.py
instrument_list_test.py
load_history.py
misc.py
mute_button.py
object_list_editor.py
open_project_dialog.py
open_project_dialog_test.py
piano.py
piano_test.py
pipeline_perf_monitor.py
player_state.py
project_registry.py
project_registry_test.py
project_view.py
project_view_test.py
project_debugger.py
property_connector.py
qled.py
qprogressindicator.py
@ -48,7 +54,7 @@ add_python_package(
render_dialog_test.py
selection_set.py
session_helpers.py
settings.py
settings_dialog.py
slots.py
stat_monitor.py
svg_symbol_filetest.py

448
noisicaa/ui/editor_app.py

@ -20,15 +20,20 @@
#
# @end:license
import asyncio
import functools
import logging
import os
import pprint
import sys
import textwrap
import traceback
import types
from typing import Any, Optional, Callable, Sequence, Type
from typing import Any, Optional, Callable, Sequence, List, Type
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import audioproc
@ -38,16 +43,19 @@ from noisicaa import core
from noisicaa import lv2
from noisicaa import editor_main_pb2
from noisicaa import runtime_settings as runtime_settings_lib
from ..exceptions import RestartAppException, RestartAppCleanException
from noisicaa import exceptions
from ..constants import EXIT_EXCEPTION, EXIT_RESTART, EXIT_RESTART_CLEAN
from .editor_window import EditorWindow
from . import editor_window
from . import audio_thread_profiler
from . import device_list
from . import project_registry
from . import pipeline_perf_monitor
from . import stat_monitor
from . import settings_dialog
from . import instrument_list
from . import instrument_library
from . import ui_base
from . import open_project_dialog
logger = logging.getLogger('ui.editor_app')
@ -59,10 +67,10 @@ class ExceptHook(object):
def __call__(
self, exc_type: Type[BaseException], exc_value: BaseException, tb: types.TracebackType
) -> None:
if issubclass(exc_type, RestartAppException):
if issubclass(exc_type, exceptions.RestartAppException):
self.app.quit(EXIT_RESTART)
return
if issubclass(exc_type, RestartAppCleanException):
if issubclass(exc_type, exceptions.RestartAppCleanException):
self.app.quit(EXIT_RESTART_CLEAN)
return
@ -104,11 +112,23 @@ class EditorApp(ui_base.AbstractEditorApp):
self.settings = settings
self.dumpSettings()
self.new_project_action = None # type: QtWidgets.QAction
self.open_project_action = None # type: QtWidgets.QAction
self.restart_action = None # type: QtWidgets.QAction
self.restart_clean_action = None # type: QtWidgets.QAction
self.crash_action = None # type: QtWidgets.QAction
self.about_action = None # type: QtWidgets.QAction
self.aboutqt_action = None # type: QtWidgets.QAction
self.show_settings_dialog_action = None # type: QtWidgets.QAction
self.show_instrument_library_action = None # type: QtWidgets.QAction
self.profile_audio_thread_action = None # type: QtWidgets.QAction
self.dump_audioproc_action = None # type: QtWidgets.QAction
self.show_pipeline_perf_monitor_action = None # type: QtWidgets.QAction
self.show_stat_monitor_action = None # type: QtWidgets.QAction
self.quit_action = None # type: QtWidgets.QAction
self.project_registry = None # type: project_registry.ProjectRegistry
self.show_edit_areas_action = None # type: QtWidgets.QAction
self.__audio_thread_profiler = None # type: audio_thread_profiler.AudioThreadProfiler
self.profile_audio_thread_action = None # type: QtWidgets.QAction
self.dump_audioproc = None # type: QtWidgets.QAction
self.audioproc_client = None # type: audioproc.AbstractAudioProcClient
self.audioproc_process = None # type: str
self.node_db = None # type: node_db.NodeDBClient
@ -116,12 +136,15 @@ class EditorApp(ui_base.AbstractEditorApp):
self.urid_mapper = None # type: lv2.ProxyURIDMapper
self.__clipboard = None # type: Any
self.__old_excepthook = None # type: Callable[[Type[BaseException], BaseException, types.TracebackType], None]
self.win = None # type: EditorWindow
self.pipeline_perf_monitor = None # type: pipeline_perf_monitor.PipelinePerfMonitor
self.stat_monitor = None # type: stat_monitor.StatMonitor
self.__windows = [] # type: List[editor_window.EditorWindow]
self.__pipeline_perf_monitor = None # type: pipeline_perf_monitor.PipelinePerfMonitor
self.__stat_monitor = None # type: stat_monitor.StatMonitor
self.default_style = None # type: str
self.instrument_list = None # type: instrument_list.InstrumentList
self.devices = None # type: device_list.DeviceList
self.setup_complete = None # type: asyncio.Event
self.__settings_dialog = None # type: settings_dialog.SettingsDialog
self.__instrument_library_dialog = None # type: instrument_library.InstrumentLibraryDialog
self.__player_state_listeners = core.CallbackMap[str, audioproc.EngineNotification]()
@ -134,89 +157,229 @@ class EditorApp(ui_base.AbstractEditorApp):
self.__old_excepthook = sys.excepthook
sys.excepthook = ExceptHook(self) # type: ignore
await self.createNodeDB()
await self.createInstrumentDB()
await self.createURIDMapper()
self.project_registry = project_registry.ProjectRegistry(
self.process.event_loop,
self.process.server,
self.process.manager,
self.node_db,
self.urid_mapper,
self.process.tmp_dir)
self.devices = device_list.DeviceList()
# TODO: 'self' is not a QObject in this context.
self.show_edit_areas_action = QtWidgets.QAction("Show Edit Areas", self.qt_app)
self.show_edit_areas_action.setCheckable(True)
self.show_edit_areas_action.triggered.connect(self.onShowEditAreasChanged)
self.show_edit_areas_action.setChecked(
bool(self.settings.value('dev/show_edit_areas', '0')))
self.__audio_thread_profiler = audio_thread_profiler.AudioThreadProfiler(
context=self.context)
self.profile_audio_thread_action = QtWidgets.QAction("Profile Audio Thread", self.qt_app)
self.profile_audio_thread_action.triggered.connect(self.onProfileAudioThread)
self.dump_audioproc = QtWidgets.QAction("Dump AudioProc", self.qt_app)
self.dump_audioproc.triggered.connect(self.onDumpAudioProc)
await self.createAudioProcProcess()
self.setup_complete = asyncio.Event(loop=self.process.event_loop)
self.default_style = self.qt_app.style().objectName()
style_name = self.settings.value('appearance/qtStyle', '')
if style_name:
# TODO: something's wrong with the QtWidgets stubs...
self.qt_app.setStyle(QtWidgets.QStyleFactory.create(style_name)) # type: ignore
logger.info("Creating PipelinePerfMonitor.")
self.pipeline_perf_monitor = pipeline_perf_monitor.PipelinePerfMonitor(context=self.context)
self.new_project_action = QtWidgets.QAction("New", self.qt_app)
self.new_project_action.setShortcut(QtGui.QKeySequence.New)
self.new_project_action.setStatusTip("Create a new project")
self.new_project_action.setEnabled(False)
self.new_project_action.triggered.connect(self.__newProject)
self.open_project_action = QtWidgets.QAction("Open", self.qt_app)
self.open_project_action.setShortcut(QtGui.QKeySequence.Open)
self.open_project_action.setStatusTip("Open an existing project")
self.open_project_action.setEnabled(False)
self.open_project_action.triggered.connect(self.__openProject)
self.restart_action = QtWidgets.QAction("Restart", self.qt_app)
self.restart_action.setShortcut("F5")
self.restart_action.setShortcutContext(Qt.ApplicationShortcut)
self.restart_action.setStatusTip("Restart the application")
self.restart_action.triggered.connect(self.__restart)
self.restart_clean_action = QtWidgets.QAction("Restart clean", self.qt_app)
self.restart_clean_action.setShortcut("Ctrl+Shift+F5")
self.restart_clean_action.setShortcutContext(Qt.ApplicationShortcut)
self.restart_clean_action.setStatusTip("Restart the application in a clean state")
self.restart_clean_action.triggered.connect(self.__restartClean)
self.crash_action = QtWidgets.QAction("Crash", self.qt_app)
self.crash_action.triggered.connect(self.__crash)
self.about_action = QtWidgets.QAction("About", self.qt_app)
self.about_action.setStatusTip("Show the application's About box")
self.about_action.triggered.connect(self.__about)
self.aboutqt_action = QtWidgets.QAction("About Qt", self.qt_app)
self.aboutqt_action.setStatusTip("Show the Qt library's About box")
self.aboutqt_action.triggered.connect(self.qt_app.aboutQt)
self.show_settings_dialog_action = QtWidgets.QAction("Settings", self.qt_app)
self.show_settings_dialog_action.setStatusTip("Open the settings dialog.")
self.show_settings_dialog_action.setEnabled(False)
self.show_settings_dialog_action.triggered.connect(self.__showSettingsDialog)
self.show_instrument_library_action = QtWidgets.QAction("Instrument Library", self.qt_app)
self.show_instrument_library_action.setStatusTip("Open the instrument library dialog.")
self.show_instrument_library_action.setEnabled(False)
self.show_instrument_library_action.triggered.connect(self.__showInstrumentLibrary)
self.profile_audio_thread_action = QtWidgets.QAction("Profile Audio Thread", self.qt_app)
self.profile_audio_thread_action.setEnabled(False)
self.profile_audio_thread_action.triggered.connect(self.__profileAudioThread)
self.dump_audioproc_action = QtWidgets.QAction("Dump AudioProc", self.qt_app)
self.dump_audioproc_action.setEnabled(False)
self.dump_audioproc_action.triggered.connect(self.__dumpAudioProc)
logger.info("Creating StatMonitor.")
self.stat_monitor = stat_monitor.StatMonitor(context=self.context)
self.show_pipeline_perf_monitor_action = QtWidgets.QAction(
"Pipeline Performance Monitor", self.qt_app)
self.show_pipeline_perf_monitor_action.setEnabled(False)
self.show_pipeline_perf_monitor_action.setCheckable(True)
</