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.
noisicaa/noisicaa/ui/editor_app.py

256 lines
7.5 KiB

#!/usr/bin/python3
import logging
import os
import sys
import traceback
from PyQt5.QtCore import QSettings, QByteArray
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QAction,
QMessageBox,
QApplication,
QStyleFactory,
QFileDialog,
)
from noisicaa import music
from ..exceptions import RestartAppException, RestartAppCleanException
from ..constants import EXIT_EXCEPTION, EXIT_RESTART, EXIT_RESTART_CLEAN
from .editor_window import EditorWindow
from .editor_project import EditorProject
from ..audioproc.pipeline import Pipeline
from ..audioproc.compose.mix import Mix
from ..audioproc.sink.pyaudio import PyAudioSink
from ..audioproc.sink.null import NullSink
from ..instr.library import InstrumentLibrary
logger = logging.getLogger('ui.editor_app')
class ExceptHook(object):
def __init__(self, app):
self.app = app
def __call__(self, exc_type, exc_value, tb):
if issubclass(exc_type, RestartAppException):
self.app.exit(EXIT_RESTART)
return
if issubclass(exc_type, RestartAppCleanException):
self.app.exit(EXIT_RESTART_CLEAN)
return
msg = ''.join(traceback.format_exception(exc_type, exc_value, tb))
logger.error("Uncaught exception:\n%s", msg)
self._show_crash_dialog(msg)
os._exit(EXIT_EXCEPTION)
def _show_crash_dialog(self, msg):
errorbox = QMessageBox()
errorbox.setWindowTitle("noisicaä crashed")
errorbox.setText("Uncaught exception")
errorbox.setInformativeText(msg)
errorbox.setIcon(QMessageBox.Critical)
errorbox.addButton("Exit", QMessageBox.AcceptRole)
errorbox.exec_()
class BaseEditorApp(QApplication):
def __init__(self, runtime_settings, settings=None):
super().__init__(['noisicaä'])
self.runtime_settings = runtime_settings
if settings is None:
settings = QSettings('odahoda.de', 'noisicaä')
if runtime_settings.start_clean:
settings.clear()
self.settings = settings
self.dumpSettings()
self._projects = []
self._exit_code = None
self.default_style = None
self.pipeline = None
self.global_mixer = None
self.sink = None
self.playback_sources = None
def setup(self):
self.default_style = self.style().objectName()
style_name = self.settings.value('appearance/qtStyle', '')
if style_name:
style = QStyleFactory.create(style_name)
self.setStyle(style)
logger.info("Creating playback pipeline.")
self.pipeline = Pipeline()
self.global_mixer = Mix('global_mixer')
self.pipeline.add_node(self.global_mixer)
self.sink = self.createAudioSink()
self.sink.inputs['in'].connect(self.global_mixer.outputs['out'])
self.playback_sources = {}
self.pipeline.start()
self.new_project_action = QAction(
"New", self,
shortcut=QKeySequence.New,
statusTip="Create a new project",
triggered=self.newProject)
self.show_edit_areas_action = QAction(
"Show Edit Areas", self,
checkable=True,
triggered=self.onShowEditAreasChanged)
self.show_edit_areas_action.setChecked(
int(self.settings.value('dev/show_edit_areas', '0')))
def cleanup(self):
logger.info("Cleaning up.")
self.pipeline.stop()
def createAudioSink(self):
sink = NullSink(sleep=0.1)
self.pipeline.set_sink(sink)
sink.setup()
return sink
def exit(self, exit_code):
logger.info("exit(%d) received", exit_code)
self._exit_code = exit_code
super().exit(exit_code)
def dumpSettings(self):
for key in self.settings.allKeys():
value = self.settings.value(key)
if isinstance(value, (bytes, QByteArray)):
value = '[%d bytes]' % len(value)
logger.info('%s: %s', key, value)
def onShowEditAreasChanged(self):
self.settings.setValue(
'dev/show_edit_areas', int(self.show_edit_areas_action.isChecked()))
self.win.updateView()
@property
def showEditAreas(self):
return (self.runtime_settings.dev_mode
and self.show_edit_areas_action.isChecked())
def addPlaybackSource(self, port):
mixer_port = self.global_mixer.append_input(port)
mixer_port.start()
self.playback_sources[port] = mixer_port
logger.info("Connected %s:%s to global mixer port %s.",
port.owner.name, port.name, mixer_port.name)
def removePlaybackSource(self, port):
mixer_port = self.playback_sources[port]
self.global_mixer.remove_input(mixer_port.name)
logger.info("Disconnected %s:%s from global mixer port %s.",
port.owner.name, port.name, mixer_port.name)
def addProject(self, project):
self.addPlaybackSource(project.master_output)
self._projects.append(project)
self.win.addProjectView(project)
self.settings.setValue(
'opened_projects',
[project.path for project in self._projects if project.path])
def removeProject(self, project):
self.win.removeProjectView(project)
self._projects.remove(project)
self.removePlaybackSource(project.master_output)
self.settings.setValue(
'opened_projects',
[project.path for project in self._projects if project.path])
def newProject(self):
path, open_filter = QFileDialog.getSaveFileName(
parent=self.win,
caption="Select Project File",
#directory=self.ui_state.get(
# 'instruments_add_dialog_path', ''),
filter="All Files (*);;noisicaä Projects (*.emp)",
#initialFilter=self.ui_state.get(
#'instruments_add_dialog_path', ''),
)
if not path:
return
project = EditorProject(self)
project.create(path)
self.addProject(project)
class EditorApp(BaseEditorApp):
def __init__(self, runtime_settings, paths, settings=None):
super().__init__(runtime_settings, settings)
self.paths = paths
self._old_excepthook = None
self.win = None
def setup(self):
logger.info("Installing custom excepthook.")
self._old_excepthook = sys.excepthook
sys.excepthook = ExceptHook(self)
super().setup()
logger.info("Creating InstrumentLibrary.")
self.instrument_library = InstrumentLibrary()
logger.info("Creating EditorWindow.")
self.win = EditorWindow(self)
self.win.show()
if self.paths:
logger.info("Starting with projects from cmdline.")
for path in self.paths:
self.win.openProject(path)
else:
reopen_projects = self.settings.value('opened_projects', [])
for path in reopen_projects:
self.win.openProject(path)
self.aboutToQuit.connect(self.shutDown)
def shutDown(self):
logger.info("Shutting down.")
self.win.storeState()
self.settings.sync()
self.dumpSettings()
def cleanup(self):
self.win.closeAll()
self.win = None
super().cleanup()
logger.info("Remove custom excepthook.")
sys.excepthook = self._old_excepthook
def createAudioSink(self):
sink = PyAudioSink()
self.pipeline.set_sink(sink)
sink.setup()
return sink