An open source DAW for GNU/Linux, inspired by modular synths. http://noisicaa.odahoda.de/
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.
 
 
 
 
 
 

406 lines
16 KiB

# @begin:license
#
# Copyright (c) 2015-2021, Ben 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 argparse
import asyncio
import functools
import os
import os.path
import signal
import sys
import time
from typing import cast, List
from PySide2.QtCore import Qt
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtQml
import flatbuffers
import qasync
import noisicaa
from noisicaa import config
from noisicaa import logging
from noisicaa import model
from noisicaa import engine as engine_lib
from noisicaa.node_lib import ui_registry
from noisicaa.node_lib import model_registry
from . import node_db
from . import qml_registry
from .DeviceDB import DeviceDB, Device
from .App import App
logger = None # type: logging.Logger
class QEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore
def __init__(self, qtApp: QtCore.QCoreApplication) -> None:
super().__init__()
self.__qtApp = qtApp
def new_event_loop(self) -> asyncio.AbstractEventLoop:
return qasync.QEventLoop(self.__qtApp)
class MainApp(App):
def __init__(self) -> None:
super().__init__()
self.__path = None # type: str
self.__logLevel = None # type: str
self.__dataDir = None # type: str
self.__libDir = None # type: str
self.__exit_code = 0
self.__quit = None # type: asyncio.Event
self.__qtApp = None # type: QtGui.QGuiApplication
self.__settings = None # type: QtCore.QSettings
self.__classRegistry = None # type: model.ClassRegistry
self.__renderLoad = [] # type: List[float]
self.__renderSampleStart = None # type: float
self.__renderBusyTime = 0
self.__renderStartTime = None # type: int
@property
def dataDir(self) -> str:
return self.__dataDir
@property
def qtApp(self) -> QtGui.QGuiApplication:
return self.__qtApp
@property
def classRegistry(self) -> model.ClassRegistry:
return self.__classRegistry
@property
def settings(self) -> QtCore.QSettings:
return self.__settings
def run(self, argv: List[str]) -> int:
self.__parse_args(argv)
with logging.LogManager(self.__logLevel):
global logger # pylint: disable=global-statement
logger = logging.getLogger('noisicaa.ui.main')
logger.info("Starting noisicaa UI...")
os.environ['QT_LOGGING_RULES'] = 'qt.scenegraph.general=true'
self.__qtApp = QtGui.QGuiApplication(argv)
self.__qtApp.setQuitOnLastWindowClosed(False)
self.__qtApp.setOrganizationName("odahoda")
self.__qtApp.setOrganizationDomain("odahoda.de")
self.__qtApp.setApplicationName("Noisicaa")
self.__qtApp.setApplicationVersion(config.VERSION)
asyncio.set_event_loop_policy(QEventLoopPolicy(self.__qtApp))
return asyncio.run(self.__runAsync())
async def __runAsync(self) -> int:
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(
sig, functools.partial(self.__handle_signal, sig))
QtCore.QDir.setSearchPaths('icons', [os.path.join(self.dataDir, 'icons')])
self.__qtApp.setWindowIcon(QtGui.QIcon('icons:noisicaa.svg'))
self.__settings = QtCore.QSettings(self.__qtApp)
with self.settingsGroup('display') as settings:
self.displayMultisampling = (settings.value('multisampling', 'true') == 'true')
self.displayMultisamplingChanged.connect(self.__displayMultisamplingChanged)
self.displayStyle = str(settings.value('style', 'Material'))
self.displayStyleChanged.connect(self.__displayStyleChanged)
self.displayMaterialTheme = str(settings.value('materialTheme', 'System'))
self.displayMaterialThemeChanged.connect(self.__displayMaterialThemeChanged)
self.displayMaterialVariant = str(settings.value('materialVariant', 'Normal'))
self.displayMaterialVariantChanged.connect(self.__displayMaterialVariantChanged)
if self.displayMultisampling:
logger.info("Enable multisampling.")
format = QtGui.QSurfaceFormat(QtGui.QSurfaceFormat.defaultFormat())
format.setSamples(16)
QtGui.QSurfaceFormat.setDefaultFormat(format)
os.environ["QT_QUICK_CONTROLS_STYLE"] = self.displayStyle
os.environ["QT_QUICK_CONTROLS_MATERIAL_VARIANT"] = self.displayMaterialVariant
self._qmlEngine = QtQml.QQmlEngine(self.__qtApp)
self._qmlEngine.warnings.connect(self.__qmlEngineWarnings)
self._qmlEngine.setOutputWarningsToStandardError(False)
self._qmlEngine.addImportPath(
os.path.abspath(os.path.join(os.path.dirname(noisicaa.__file__), '..')))
qml_registry.registerTypes()
self._modelRegistry = model_registry.ModelRegistry()
self._uiRegistry = ui_registry.UIRegistry()
self.__classRegistry = model.ClassRegistry()
for cls in self._modelRegistry.classes.values():
self.__classRegistry.add(cls)
for cls in self._modelRegistry.extraClasses:
self.__classRegistry.add(cls)
self.__quit = asyncio.Event()
try:
self._deviceDB = DeviceDB(self)
logger.info("Initialize engine...")
self._engine = engine_lib.Engine()
self._engine.notifications.add(self.__handleEngineNotification)
self.engineMasterVolume = float(str(settings.value('masterVolume', '1.0')))
logger.info("Initialize node db...")
builder = flatbuffers.Builder(1024)
builder.Finish(engine_lib.CreateNodeDBSettings(
builder,
search_path_builtins=[os.path.join(self.__libDir, 'noisicaa/node_lib')]))
self._nodeDB = node_db.NodeDB(builder.Output(), self.__qtApp)
await self._nodeDB.setup()
logger.info("Configure backend...")
self.updateAudioBackend()
self.needsRestart = False
self.needsRestartChanged.connect(functools.partial(logger.error, "needs restart %s"))
recentProjects = settings.value('recentProjects', [])
if isinstance(recentProjects, str):
recentProjects = [recentProjects]
assert isinstance(recentProjects, list)
self._recentProjects.setPaths(recentProjects)
if self.__path is not None:
self.openProject(self.__path)
else:
self.newProject()
self._projectWindowComponent = self.createQmlComponent(
os.path.join(os.path.dirname(__file__), 'ProjectWindow.qml'))
self._window = self._projectWindowComponent.createWithInitialProperties(
{'d': self._project})
self.__startRenderPerformanceMonitor()
with self.settingsGroup('window/mainwindow') as settings:
if settings.value('fullscreen', 'false') == 'true':
self._window.showFullScreen()
elif settings.value('maximized', 'false') == 'true':
self._window.showMaximized()
else:
x = cast(str, settings.value('x'))
y = cast(str, settings.value('y'))
if x is not None and y is not None:
self._window.setPosition(int(x), int(y))
width = cast(str, settings.value('width'))
height = cast(str, settings.value('height'))
if width is not None and height is not None:
self._window.resize(int(width), int(height))
self._window.show()
self._project.restartEngine()
await self.__quit.wait()
logger.info("Closing UI...")
finally:
if self._qmlEngine is not None:
# Delete the QQmlEngine before the QML properties are cleared out, which triggers a
# whole bunch of warning messages. See also
# https://bugreports.qt.io/browse/QTBUG-81247
self._qmlEngine.deleteLater()
await asyncio.sleep(0)
if self._window is not None:
with self.settingsGroup('window/mainwindow') as settings:
geometry = self._window.geometry()
settings.setValue('x', geometry.x())
settings.setValue('y', geometry.y())
settings.setValue('width', geometry.width())
settings.setValue('height', geometry.height())
states = self._window.windowStates()
settings.setValue('maximized', bool(states & Qt.WindowMaximized))
settings.setValue('fullscreen', bool(states & Qt.WindowFullScreen))
self._window.close()
self._window = None
if self._project is not None:
self._project.cleanup()
if self._engine is not None:
self._engine.stop()
if self._nodeDB is not None:
await self._nodeDB.cleanup()
return self.__exit_code
def __parse_args(self, argv: List[str]) -> None:
defaultDataDir = config.DATA_DIR
defaultLibDir = config.LIB_DIR
assert defaultLibDir.startswith('/')
if 'APPDIR' in os.environ:
assert defaultDataDir.startswith('/')
defaultDataDir = os.path.join(os.environ['APPDIR'], defaultDataDir[1:])
assert defaultLibDir.startswith('/')
defaultLibDir = os.path.join(os.environ['APPDIR'], defaultLibDir[1:])
parser = argparse.ArgumentParser(
prog=argv[0])
parser.add_argument(
'--log-level',
metavar='LEVEL',
default='error',
help=("Minimum level for log messages written to STDERR. A comma separated list"
" of logger=level pairs, where 'logger' is the name of a logger and 'level'"
" one of 'debug', 'info', 'warning', 'error', 'critical'. A bare 'level'"
" applies to the root logger. E.g. 'error,noisicaa.ui=info' will print"
" INFO level logs for 'noisicaa.ui' and ERROR level for all other logger."))
parser.add_argument(
'--data-dir',
metavar='PATH',
default=defaultDataDir,
help="Path to noisicaa's data files (default: {})".format(config.DATA_DIR))
parser.add_argument(
'--lib-dir',
metavar='PATH',
default=defaultLibDir,
help="Path to noisicaa's libraries (default: {})".format(config.LIB_DIR))
if 'APPDIR' in os.environ:
parser.add_argument(
'--shell',
action='store_true',
help=argparse.SUPPRESS,
default=False)
parser.add_argument(
'path',
metavar="PROJECT",
nargs='?',
help="Project file to open.")
# self.runtime_settings.init_argparser(parser)
args = parser.parse_args(args=argv[1:])
# self.runtime_settings.set_from_args(args)
self.__path = args.path
self.__logLevel = args.log_level
self.__dataDir = args.data_dir
if not self.__dataDir.endswith('/'):
self.__dataDir += '/'
self.__libDir = args.lib_dir
if not self.__libDir.endswith('/'):
self.__libDir += '/'
if getattr(args, 'shell', False):
os.chdir(os.environ['APPDIR'])
os.execlp('bash', 'bash')
def __qmlEngineWarnings(self, warnings: List[QtQml.QQmlError]) -> None:
qmlLogger = logging.getLogger('noisicaa.ui.qml')
for warning in warnings:
qmlLogger.warning(warning.toString())
def __handle_signal(self, sig: signal.Signals) -> None:
logger.info("%s received.", sig.name)
self.quit(1)
def __handleEngineNotification(self, notification: engine_lib.EngineNotification) -> None:
if notification.msg_type == engine_lib.EngineNotificationPayload.engine_state_change:
engine_state_change = engine_lib.EngineStateChange(notification.msg)
state = engine_lib.EngineState(engine_state_change.state)
logger.info("Engine state changed to %s", state.name)
elif notification.msg_type == engine_lib.EngineNotificationPayload.block_stats:
block_stats = engine_lib.BlockStats(notification.msg)
logger.debug("Load: %.3f%%", 100.0 * block_stats.load)
elif notification.msg_type == engine_lib.EngineNotificationPayload.device_added:
client = engine_lib.DeviceAdded(notification.msg)
device = Device(client.name)
for port in client.ports:
device.addPort(
port.name,
engine_lib.PortDirection(port.direction),
engine_lib.PortType(port.type))
self._deviceDB.addDevice(device)
else:
logger.error("Unhandled notification %s", notification.msg_type)
def __displayMultisamplingChanged(self, v: bool) -> None:
self.__settings.setValue('display/multisampling', 'true' if v else 'false')
self.needsRestart = True
def __displayStyleChanged(self, v: str) -> None:
self.__settings.setValue('display/style', v)
self.needsRestart = True
def __displayMaterialThemeChanged(self, v: str) -> None:
self.__settings.setValue('display/materialTheme', v)
def __displayMaterialVariantChanged(self, v: str) -> None:
self.__settings.setValue('display/materialVariant', v)
self.needsRestart = True
def __startRenderPerformanceMonitor(self) -> None:
if not self.enableRenderPerformanceMonitoring:
return
self.__renderLoad = []
self.__renderSampleStart = None
self.__renderBusyTime = 0
self.__renderStartTime = None
self._window.beforeSynchronizing.connect(self.__frameStarted, Qt.DirectConnection)
self._window.afterRendering.connect(self.__frameFinished, Qt.DirectConnection)
def __frameStarted(self) -> None:
self.__renderStartTime = time.perf_counter_ns()
def __frameFinished(self) -> None:
if self.__renderStartTime is not None:
self.__renderBusyTime += time.perf_counter_ns() - self.__renderStartTime
self.__renderStartTime = None
now = time.time()
if self.__renderSampleStart is None:
self.__renderSampleStart = now
elif now >= self.__renderSampleStart + 0.25:
self.__renderLoad.append(
100 * self.__renderBusyTime / (1e9 * (now - self.__renderSampleStart)))
self.__renderBusyTime = 0
while len(self.__renderLoad) > 1000:
self.__renderLoad.pop(0)
print('{:2f}%'.format(self.__renderLoad[-1]))
self.__renderSampleStart = now
def quit(self, exit_code: int = 0) -> None:
self.__exit_code = exit_code
self.__quit.set()
if __name__ == '__main__':
app = MainApp()
rc = app.run(sys.argv)
sys.exit(rc)