noisicaa/noisicaa/ui/editor_app.py

612 lines
26 KiB
Python
Raw Normal View History

#!/usr/bin/python3
2017-10-04 15:03:38 +02:00
# @begin:license
#
2019-01-12 01:20:46 +01:00
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
2017-10-04 15:03:38 +02:00
#
# 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 functools
import logging
import os
import pprint
import sys
import textwrap
import traceback
import types
from typing import Any, Optional, Callable, Sequence, List, Type
2019-06-02 16:05:31 +02:00
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import audioproc
2016-09-19 19:30:06 +02:00
from noisicaa import instrument_db
from noisicaa import node_db
from noisicaa import core
from noisicaa import lv2
from noisicaa import editor_main_pb2
from noisicaa import runtime_settings as runtime_settings_lib
from noisicaa import exceptions
from ..constants import EXIT_EXCEPTION, EXIT_RESTART, EXIT_RESTART_CLEAN
from . import editor_window
from . import audio_thread_profiler
Rewrite MIDI input. - A new "MIDI source" node, which can feed MIDI events from any ALSA device into the graph. - All done rt safe in C++. The older Python implementation in noisicaa.devices has been removed. - Still quite hacky: - It's built into the portaudio backend. There should probably be a separate "ALSA sequencer" backend for this, with the option to have an alternative "Jack MIDI" backend. But for that I have to figure out how to have separate backends for audio and MIDI and how that interacts for all possible combinations. - It just blindly connects to all readable MIDI ports, collecting all events. Filtering out the events for a specific port (e.g. a MIDI keyboard) happens in the "MIDI source" node. The engine should track, which ports are being used and only connect to those. Squashed commit of the following: commit c811be510347d1fd23abea081ba0a4d93e8cb6bf Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:36:18 2019 +0100 Move ALSADeviceManager to a separate file. commit 6e5d9a2c691fdf639f0173b9dd2ebfde7f58f4f4 Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:25:29 2019 +0100 Fix/improve tests. commit 94b4fa253f8a4f8a84d13dd718dbaeac99fee5fe Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 13:57:07 2019 +0100 Reanimate playback from the instrument library. commit 17a288980fc361f190876763dbe4a6a6bbd0c2b3 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:57:54 2019 +0100 Remove the now obsolete noisicaa.devices package. commit aa2f9bbc1ae61295157f66948b276861dee00379 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:45:50 2019 +0100 Strip the PianoWidget down to just the keys. commit 1c87b29f7abb51defa28b33f902f8de85ae7eb55 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:24:57 2019 +0100 Add piano to MIDI source node. - Make BasePipelineGraphNode.pipeline_node_id globally available. - Allow sending processor messages from the UI. - Pass the MIDI events to the rt thread via a FIFO queue. commit f19114e966ab2d9261fd3a86b93d2ca88e9f3fba Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 11:29:43 2019 +0100 Remove the System Out node again. And the related Backend::input(). Not needed after all. commit a839f259e3b8e338072be9c8b9fa58d8dc0d36a4 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 10:03:33 2019 +0100 Wire up MIDI source to events from the backend. - Make the event buffer accessible via the block context. - Backend creates sequence of (uri, midi) tuples. - ProcessorMidiSource filters that list and emit list of midi events. commit 347dc0168b00315eed233fdec40c8a9d6b5ffe41 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:21:36 2019 +0100 Make the main ALSA sequences listen to all output ports again. Now also tracking new ports as they appear. commit 86b6b7a59974c18c6078761fe1010456e5f26e43 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:20:52 2019 +0100 Bug fix. commit 776dbd4a946ecfa8e178cd7e3e108a4c3519f3cb Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:43:47 2019 +0100 Editor tracks devices in a QAbstractItemModel. And MIDI Source node uses that for the port selector combobox. commit a9c578e377948d187a0ee8ede90c29cc32b337a1 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:42:30 2019 +0100 Also handle port changes somewhat gracefully. commit f4cd8c7535b36e7c6b9323ff2861d72e376bac08 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 05:34:01 2019 +0100 Also handle CC events. commit 1329e51ff9747764a2bb5c6578f3490047cee135 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 04:30:45 2019 +0100 Device manager that tracks ALSA sequencer clients. - Allow backends to post engine notifications. - PortAudioBackend runs a separate (non-rt) thread, with a sequencer client just for listening for client notifications. - Create a DeviceDescription proto for ALSA sequencer clients and post them as engine notifications. commit 10c5b827de47479e6a8046c44cd32494693c762b Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 01:34:47 2019 +0100 A MIDI source node, which doesn't really do anything yet. commit e09a5c70e3b950f3c6e30b81c2e8b67d65a947b3 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 11:51:36 2019 +0100 Use C string for Spec::get_buffer_idx() to avoid malloc in the audio thread. commit 24cfffdf60a4ad888e65fe839165666ebef0f9f0 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:13:26 2019 +0100 Add a "System In" node to the graph and wire it up to the MIDI events from the backend. Also rename "Audio Out" to "System Out", because that makes more sense, now that there is more than audio being passed around. commit 77be27b0e487b0830d913bdcc54cf56ea35114cf Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:10:55 2019 +0100 Add Backend::input() method to read incoming MIDI events. Also switch to an enum for the channel arg. commit 5c4acefc476ace640d8a0ac40d6816ca48399207 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:08:20 2019 +0100 PortAudioBackend also reads MIDI events into a buffer. Very prototypish implementation. It just scans for all available devices and connects to their outputs. Still need to think about how to deal with different devices.
2019-01-14 04:02:26 +01:00
from . import device_list
from . import project_registry
2016-07-17 00:17:25 +02:00
from . import pipeline_perf_monitor
2016-11-27 21:31:15 +01:00
from . import stat_monitor
2019-06-02 16:05:31 +02:00
from . import settings_dialog
from . import instrument_list
2019-06-02 16:53:49 +02:00
from . import instrument_library
from . import ui_base
from . import open_project_dialog
logger = logging.getLogger('ui.editor_app')
class ExceptHook(object):
def __init__(self, app: 'EditorApp') -> None:
self.app = app
def __call__(
self, exc_type: Type[BaseException], exc_value: BaseException, tb: types.TracebackType
) -> None:
if issubclass(exc_type, exceptions.RestartAppException):
self.app.quit(EXIT_RESTART)
return
if issubclass(exc_type, exceptions.RestartAppCleanException):
self.app.quit(EXIT_RESTART_CLEAN)
return
msg = ''.join(traceback.format_exception(exc_type, exc_value, tb))
logger.error("Uncaught exception:\n%s", msg)
self.app.crashWithMessage("Uncaught exception", msg)
class QApplication(QtWidgets.QApplication):
def __init__(self) -> None:
super().__init__(['noisicaä'])
self.setQuitOnLastWindowClosed(False)
class EditorApp(ui_base.AbstractEditorApp):
def __init__(
self, *,
qt_app: QtWidgets.QApplication,
process: core.ProcessBase,
paths: Sequence[str],
runtime_settings: runtime_settings_lib.RuntimeSettings,
settings: Optional[QtCore.QSettings] = None
) -> None:
self.__context = ui_base.CommonContext(app=self)
super().__init__()
2016-06-23 00:49:45 +02:00
self.paths = paths
self.qt_app = qt_app
self.process = process
self.runtime_settings = runtime_settings
if settings is None:
settings = QtCore.QSettings('odahoda.de', 'noisicaä')
if runtime_settings.start_clean:
settings.clear()
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.__audio_thread_profiler = None # type: audio_thread_profiler.AudioThreadProfiler
self.audioproc_client = None # type: audioproc.AbstractAudioProcClient
self.audioproc_process = None # type: str
self.node_db = None # type: node_db.NodeDBClient
self.instrument_db = None # type: instrument_db.InstrumentDBClient
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.__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
Rewrite MIDI input. - A new "MIDI source" node, which can feed MIDI events from any ALSA device into the graph. - All done rt safe in C++. The older Python implementation in noisicaa.devices has been removed. - Still quite hacky: - It's built into the portaudio backend. There should probably be a separate "ALSA sequencer" backend for this, with the option to have an alternative "Jack MIDI" backend. But for that I have to figure out how to have separate backends for audio and MIDI and how that interacts for all possible combinations. - It just blindly connects to all readable MIDI ports, collecting all events. Filtering out the events for a specific port (e.g. a MIDI keyboard) happens in the "MIDI source" node. The engine should track, which ports are being used and only connect to those. Squashed commit of the following: commit c811be510347d1fd23abea081ba0a4d93e8cb6bf Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:36:18 2019 +0100 Move ALSADeviceManager to a separate file. commit 6e5d9a2c691fdf639f0173b9dd2ebfde7f58f4f4 Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:25:29 2019 +0100 Fix/improve tests. commit 94b4fa253f8a4f8a84d13dd718dbaeac99fee5fe Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 13:57:07 2019 +0100 Reanimate playback from the instrument library. commit 17a288980fc361f190876763dbe4a6a6bbd0c2b3 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:57:54 2019 +0100 Remove the now obsolete noisicaa.devices package. commit aa2f9bbc1ae61295157f66948b276861dee00379 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:45:50 2019 +0100 Strip the PianoWidget down to just the keys. commit 1c87b29f7abb51defa28b33f902f8de85ae7eb55 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:24:57 2019 +0100 Add piano to MIDI source node. - Make BasePipelineGraphNode.pipeline_node_id globally available. - Allow sending processor messages from the UI. - Pass the MIDI events to the rt thread via a FIFO queue. commit f19114e966ab2d9261fd3a86b93d2ca88e9f3fba Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 11:29:43 2019 +0100 Remove the System Out node again. And the related Backend::input(). Not needed after all. commit a839f259e3b8e338072be9c8b9fa58d8dc0d36a4 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 10:03:33 2019 +0100 Wire up MIDI source to events from the backend. - Make the event buffer accessible via the block context. - Backend creates sequence of (uri, midi) tuples. - ProcessorMidiSource filters that list and emit list of midi events. commit 347dc0168b00315eed233fdec40c8a9d6b5ffe41 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:21:36 2019 +0100 Make the main ALSA sequences listen to all output ports again. Now also tracking new ports as they appear. commit 86b6b7a59974c18c6078761fe1010456e5f26e43 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:20:52 2019 +0100 Bug fix. commit 776dbd4a946ecfa8e178cd7e3e108a4c3519f3cb Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:43:47 2019 +0100 Editor tracks devices in a QAbstractItemModel. And MIDI Source node uses that for the port selector combobox. commit a9c578e377948d187a0ee8ede90c29cc32b337a1 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:42:30 2019 +0100 Also handle port changes somewhat gracefully. commit f4cd8c7535b36e7c6b9323ff2861d72e376bac08 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 05:34:01 2019 +0100 Also handle CC events. commit 1329e51ff9747764a2bb5c6578f3490047cee135 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 04:30:45 2019 +0100 Device manager that tracks ALSA sequencer clients. - Allow backends to post engine notifications. - PortAudioBackend runs a separate (non-rt) thread, with a sequencer client just for listening for client notifications. - Create a DeviceDescription proto for ALSA sequencer clients and post them as engine notifications. commit 10c5b827de47479e6a8046c44cd32494693c762b Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 01:34:47 2019 +0100 A MIDI source node, which doesn't really do anything yet. commit e09a5c70e3b950f3c6e30b81c2e8b67d65a947b3 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 11:51:36 2019 +0100 Use C string for Spec::get_buffer_idx() to avoid malloc in the audio thread. commit 24cfffdf60a4ad888e65fe839165666ebef0f9f0 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:13:26 2019 +0100 Add a "System In" node to the graph and wire it up to the MIDI events from the backend. Also rename "Audio Out" to "System Out", because that makes more sense, now that there is more than audio being passed around. commit 77be27b0e487b0830d913bdcc54cf56ea35114cf Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:10:55 2019 +0100 Add Backend::input() method to read incoming MIDI events. Also switch to an enum for the channel arg. commit 5c4acefc476ace640d8a0ac40d6816ca48399207 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:08:20 2019 +0100 PortAudioBackend also reads MIDI events into a buffer. Very prototypish implementation. It just scans for all available devices and connects to their outputs. Still need to think about how to deal with different devices.
2019-01-14 04:02:26 +01:00
self.devices = None # type: device_list.DeviceList
self.setup_complete = None # type: asyncio.Event
2019-06-02 16:05:31 +02:00
self.__settings_dialog = None # type: settings_dialog.SettingsDialog
2019-06-02 16:53:49 +02:00
self.__instrument_library_dialog = None # type: instrument_library.InstrumentLibraryDialog
2019-06-02 16:05:31 +02:00
self.__player_state_listeners = core.CallbackMap[str, audioproc.EngineNotification]()
@property
def context(self) -> ui_base.CommonContext:
return self.__context
async def setup(self) -> None:
logger.info("Installing custom excepthook.")
self.__old_excepthook = sys.excepthook
sys.excepthook = ExceptHook(self) # type: ignore
self.setup_complete = asyncio.Event(loop=self.process.event_loop)
2019-06-02 16:05:31 +02:00
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
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)
2019-06-02 16:05:31 +02:00
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)
2019-06-02 16:53:49 +02:00
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)
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)
self.show_stat_monitor_action = QtWidgets.QAction("Stat Monitor", self.qt_app)
self.show_stat_monitor_action.setEnabled(False)
self.show_stat_monitor_action.setCheckable(True)
2019-06-02 16:05:31 +02:00
self.quit_action = QtWidgets.QAction("Quit", self.qt_app)
self.quit_action.setShortcut(QtGui.QKeySequence.Quit)
self.quit_action.setShortcutContext(Qt.ApplicationShortcut)
self.quit_action.setStatusTip("Quit the application")
self.quit_action.triggered.connect(self.quit)
logger.info("Creating initial window...")
win = await self.createWindow()
tab_page = win.addProjectTab()
progress = win.createSetupProgress()
try:
progress.setNumSteps(5)
logger.info("Creating StatMonitor.")
self.__stat_monitor = stat_monitor.StatMonitor(context=self.context)
self.show_stat_monitor_action.setChecked(self.__stat_monitor.isVisible())
self.show_stat_monitor_action.toggled.connect(
self.__stat_monitor.setVisible)
self.__stat_monitor.visibilityChanged.connect(
self.show_stat_monitor_action.setChecked)
logger.info("Creating SettingsDialog...")
self.__settings_dialog = settings_dialog.SettingsDialog(context=self.context)
with progress.step("Scanning projects..."):
self.project_registry = project_registry.ProjectRegistry(context=self.context)
await self.project_registry.setup()
initial_projects = []
if self.paths:
for path in self.paths:
if path.startswith('+'):
initial_projects.append((True, path[1:]))
else:
initial_projects.append((False, path))
else:
for path in self.settings.value('opened_projects', []) or []:
initial_projects.append((False, path))
logger.info(
"Starting with projects:\n%s",
'\n'.join('%s%s' % ('+' if create else '', path)
for create, path in initial_projects))
idx = 0
for create, path in initial_projects:
if idx == 0:
tab = tab_page
else:
tab = win.addProjectTab()
if create:
self.process.event_loop.create_task(tab.createProject(path))
idx += 1
else:
try:
project = self.project_registry.getProject(path)
except KeyError:
logging.error("There is no known project at %s", path)
else:
self.process.event_loop.create_task(tab.openProject(project))
idx += 1
if idx == 0:
tab_page.showOpenDialog()
with progress.step("Scanning nodes and plugins..."):
await self.createNodeDB()
with progress.step("Creating URID mapper..."):
await self.createURIDMapper()
with progress.step("Setting up audio engine..."):
self.devices = device_list.DeviceList()
await self.createAudioProcProcess()
win.audioprocReady()
logger.info("Creating AudioThreadProfiler...")
self.__audio_thread_profiler = audio_thread_profiler.AudioThreadProfiler(
context=self.context)
logger.info("Creating PipelinePerfMonitor...")
self.__pipeline_perf_monitor = pipeline_perf_monitor.PipelinePerfMonitor(
context=self.context)
self.show_pipeline_perf_monitor_action.setChecked(
self.__pipeline_perf_monitor.isVisible())
self.show_pipeline_perf_monitor_action.toggled.connect(
self.__pipeline_perf_monitor.setVisible)
self.__pipeline_perf_monitor.visibilityChanged.connect(
self.show_pipeline_perf_monitor_action.setChecked)
2019-06-02 16:53:49 +02:00
with progress.step("Scanning instruments..."):
create_instrument_db_response = editor_main_pb2.CreateProcessResponse()
await self.process.manager.call(
'CREATE_INSTRUMENT_DB_PROCESS', None, create_instrument_db_response)
instrument_db_address = create_instrument_db_response.address
self.instrument_db = instrument_db.InstrumentDBClient(
self.process.event_loop, self.process.server)
self.instrument_list = instrument_list.InstrumentList(context=self.context)
self.instrument_list.setup()
await self.instrument_db.setup()
await self.instrument_db.connect(instrument_db_address)
2019-06-02 16:53:49 +02:00
self.__instrument_library_dialog = instrument_library.InstrumentLibraryDialog(
context=self.context)
await self.__instrument_library_dialog.setup()
finally:
win.deleteSetupProgress()
self.new_project_action.setEnabled(True)
self.open_project_action.setEnabled(True)
2019-06-02 16:05:31 +02:00
self.show_settings_dialog_action.setEnabled(True)
2019-06-02 16:53:49 +02:00
self.show_instrument_library_action.setEnabled(True)
self.profile_audio_thread_action.setEnabled(True)
self.dump_audioproc_action.setEnabled(True)
self.show_pipeline_perf_monitor_action.setEnabled(True)
self.show_stat_monitor_action.setEnabled(True)
self.setup_complete.set()
async def cleanup(self) -> None:
logger.info("Cleanup app...")
if self.__stat_monitor is not None:
self.__stat_monitor.storeState()
self.__stat_monitor = None
if self.__pipeline_perf_monitor is not None:
self.__pipeline_perf_monitor.storeState()
self.__pipeline_perf_monitor = None
if self.__audio_thread_profiler is not None:
self.__audio_thread_profiler.hide()
self.__audio_thread_profiler = None
2019-06-02 16:53:49 +02:00
if self.__instrument_library_dialog is not None:
await self.__instrument_library_dialog.cleanup()
self.__instrument_library_dialog = None
2019-06-02 16:05:31 +02:00
if self.__settings_dialog is not None:
self.__settings_dialog.storeState()
self.__settings_dialog.close()
self.__settings_dialog = None
while self.__windows:
win = self.__windows.pop(0)
win.storeState()
await win.cleanup()
if self.project_registry is not None:
await self.project_registry.cleanup()
self.project_registry = None
if self.audioproc_client is not None:
await self.audioproc_client.disconnect()
await self.audioproc_client.cleanup()
self.audioproc_client = None
if self.audioproc_process is not None:
await self.process.manager.call(
'SHUTDOWN_PROCESS',
editor_main_pb2.ShutdownProcessRequest(
address=self.audioproc_process))
self.audioproc_process = None
if self.urid_mapper is not None:
await self.urid_mapper.cleanup(self.process.event_loop)
self.urid_mapper = None
if self.instrument_list is not None:
self.instrument_list.cleanup()
self.instrument_list = None
2016-09-19 19:30:06 +02:00
if self.instrument_db is not None:
await self.instrument_db.disconnect()
2016-09-19 19:30:06 +02:00
await self.instrument_db.cleanup()
self.instrument_db = None
if self.node_db is not None:
await self.node_db.disconnect()
2016-09-19 19:30:06 +02:00
await self.node_db.cleanup()
self.node_db = None
self.settings.sync()
self.dumpSettings()
logger.info("Remove custom excepthook.")
sys.excepthook = self.__old_excepthook # type: ignore
async def createWindow(self) -> editor_window.EditorWindow:
win = editor_window.EditorWindow(context=self.context)
await win.setup()
self.__windows.append(win)
return win
async def deleteWindow(self, win: editor_window.EditorWindow) -> None:
self.__windows.remove(win)
await win.cleanup()
if not self.__windows:
self.quit()
def quit(self, exit_code: int = 0) -> None:
# TODO: quit() is not a method of ProcessBase, only in UIProcess. Find some way to
# fix that without a cyclic import.
self.process.quit(exit_code) # type: ignore
2016-06-23 00:49:45 +02:00
async def createAudioProcProcess(self) -> None:
create_audioproc_request = editor_main_pb2.CreateAudioProcProcessRequest(
name='main',
host_parameters=audioproc.HostParameters(
block_size=2 ** int(self.settings.value('audio/block_size', 10)),
sample_rate=int(self.settings.value('audio/sample_rate', 44100))))
create_audioproc_response = editor_main_pb2.CreateProcessResponse()
await self.process.manager.call(
'CREATE_AUDIOPROC_PROCESS', create_audioproc_request, create_audioproc_response)
self.audioproc_process = create_audioproc_response.address
self.audioproc_client = audioproc.AudioProcClient(
self.process.event_loop, self.process.server, self.urid_mapper)
self.audioproc_client.engine_notifications.add(self.__handleEngineNotification)
await self.audioproc_client.setup()
await self.audioproc_client.connect(
self.audioproc_process, {'perf_data'})
await self.audioproc_client.create_realm(name='root')
await self.audioproc_client.set_backend(
self.settings.value('audio/backend', 'portaudio'),
)
2016-07-03 19:50:13 +02:00
async def createNodeDB(self) -> None:
create_node_db_response = editor_main_pb2.CreateProcessResponse()
await self.process.manager.call(
'CREATE_NODE_DB_PROCESS', None, create_node_db_response)
node_db_address = create_node_db_response.address
self.node_db = node_db.NodeDBClient(self.process.event_loop, self.process.server)
await self.node_db.setup()
await self.node_db.connect(node_db_address)
async def createURIDMapper(self) -> None:
create_urid_mapper_response = editor_main_pb2.CreateProcessResponse()
await self.process.manager.call(
'CREATE_URID_MAPPER_PROCESS', None, create_urid_mapper_response)
urid_mapper_address = create_urid_mapper_response.address
self.urid_mapper = lv2.ProxyURIDMapper(
server_address=urid_mapper_address,
tmp_dir=self.process.tmp_dir)
await self.urid_mapper.setup(self.process.event_loop)
def dumpSettings(self) -> None:
for key in self.settings.allKeys():
value = self.settings.value(key)
if isinstance(value, (bytes, QtCore.QByteArray)):
value = '[%d bytes]' % len(value)
logger.info('%s: %s', key, value)
def __handleEngineNotification(self, msg: audioproc.EngineNotification) -> None:
Rewrite MIDI input. - A new "MIDI source" node, which can feed MIDI events from any ALSA device into the graph. - All done rt safe in C++. The older Python implementation in noisicaa.devices has been removed. - Still quite hacky: - It's built into the portaudio backend. There should probably be a separate "ALSA sequencer" backend for this, with the option to have an alternative "Jack MIDI" backend. But for that I have to figure out how to have separate backends for audio and MIDI and how that interacts for all possible combinations. - It just blindly connects to all readable MIDI ports, collecting all events. Filtering out the events for a specific port (e.g. a MIDI keyboard) happens in the "MIDI source" node. The engine should track, which ports are being used and only connect to those. Squashed commit of the following: commit c811be510347d1fd23abea081ba0a4d93e8cb6bf Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:36:18 2019 +0100 Move ALSADeviceManager to a separate file. commit 6e5d9a2c691fdf639f0173b9dd2ebfde7f58f4f4 Author: Ben Niemann <pink@odahoda.de> Date: Mon Jan 14 03:25:29 2019 +0100 Fix/improve tests. commit 94b4fa253f8a4f8a84d13dd718dbaeac99fee5fe Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 13:57:07 2019 +0100 Reanimate playback from the instrument library. commit 17a288980fc361f190876763dbe4a6a6bbd0c2b3 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:57:54 2019 +0100 Remove the now obsolete noisicaa.devices package. commit aa2f9bbc1ae61295157f66948b276861dee00379 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:45:50 2019 +0100 Strip the PianoWidget down to just the keys. commit 1c87b29f7abb51defa28b33f902f8de85ae7eb55 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 12:24:57 2019 +0100 Add piano to MIDI source node. - Make BasePipelineGraphNode.pipeline_node_id globally available. - Allow sending processor messages from the UI. - Pass the MIDI events to the rt thread via a FIFO queue. commit f19114e966ab2d9261fd3a86b93d2ca88e9f3fba Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 11:29:43 2019 +0100 Remove the System Out node again. And the related Backend::input(). Not needed after all. commit a839f259e3b8e338072be9c8b9fa58d8dc0d36a4 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 10:03:33 2019 +0100 Wire up MIDI source to events from the backend. - Make the event buffer accessible via the block context. - Backend creates sequence of (uri, midi) tuples. - ProcessorMidiSource filters that list and emit list of midi events. commit 347dc0168b00315eed233fdec40c8a9d6b5ffe41 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:21:36 2019 +0100 Make the main ALSA sequences listen to all output ports again. Now also tracking new ports as they appear. commit 86b6b7a59974c18c6078761fe1010456e5f26e43 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 09:20:52 2019 +0100 Bug fix. commit 776dbd4a946ecfa8e178cd7e3e108a4c3519f3cb Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:43:47 2019 +0100 Editor tracks devices in a QAbstractItemModel. And MIDI Source node uses that for the port selector combobox. commit a9c578e377948d187a0ee8ede90c29cc32b337a1 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 08:42:30 2019 +0100 Also handle port changes somewhat gracefully. commit f4cd8c7535b36e7c6b9323ff2861d72e376bac08 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 05:34:01 2019 +0100 Also handle CC events. commit 1329e51ff9747764a2bb5c6578f3490047cee135 Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 04:30:45 2019 +0100 Device manager that tracks ALSA sequencer clients. - Allow backends to post engine notifications. - PortAudioBackend runs a separate (non-rt) thread, with a sequencer client just for listening for client notifications. - Create a DeviceDescription proto for ALSA sequencer clients and post them as engine notifications. commit 10c5b827de47479e6a8046c44cd32494693c762b Author: Ben Niemann <pink@odahoda.de> Date: Sun Jan 13 01:34:47 2019 +0100 A MIDI source node, which doesn't really do anything yet. commit e09a5c70e3b950f3c6e30b81c2e8b67d65a947b3 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 11:51:36 2019 +0100 Use C string for Spec::get_buffer_idx() to avoid malloc in the audio thread. commit 24cfffdf60a4ad888e65fe839165666ebef0f9f0 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:13:26 2019 +0100 Add a "System In" node to the graph and wire it up to the MIDI events from the backend. Also rename "Audio Out" to "System Out", because that makes more sense, now that there is more than audio being passed around. commit 77be27b0e487b0830d913bdcc54cf56ea35114cf Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:10:55 2019 +0100 Add Backend::input() method to read incoming MIDI events. Also switch to an enum for the channel arg. commit 5c4acefc476ace640d8a0ac40d6816ca48399207 Author: Ben Niemann <pink@odahoda.de> Date: Sat Jan 12 08:08:20 2019 +0100 PortAudioBackend also reads MIDI events into a buffer. Very prototypish implementation. It just scans for all available devices and connects to their outputs. Still need to think about how to deal with different devices.
2019-01-14 04:02:26 +01:00
for device_manager_message in msg.device_manager_messages:
action = device_manager_message.WhichOneof('action')
if action == 'added':
self.devices.addDevice(device_manager_message.added)
elif action == 'removed':
self.devices.removeDevice(device_manager_message.removed)
else:
raise ValueError(action)
def __newProject(self) -> None:
win = self.__windows[0]
dialog = open_project_dialog.NewProjectDialog(parent=win, context=self.context)
dialog.setModal(True)
dialog.finished.connect(functools.partial(self.__newProjectDialogDone, dialog, win))
dialog.show()
def __newProjectDialogDone(self, dialog: open_project_dialog.NewProjectDialog, win: editor_window.EditorWindow, result: int) -> None:
if result != QtWidgets.QDialog.Accepted:
return
path = dialog.projectPath()
tab = win.addProjectTab()
self.process.event_loop.create_task(tab.createProject(path))
def __openProject(self) -> None:
win = self.__windows[0]
tab = win.addProjectTab()
tab.showOpenDialog()
def __about(self) -> None:
QtWidgets.QMessageBox.about(
None, "About noisicaä",
textwrap.dedent("""\
Some text goes here...
"""))
def __restart(self) -> None:
raise exceptions.RestartAppException("Restart requested by user.")
def __restartClean(self) -> None:
raise exceptions.RestartAppCleanException("Clean restart requested by user.")
def __crash(self) -> None:
raise RuntimeError("Something bad happened")
2019-06-02 16:53:49 +02:00
def __showInstrumentLibrary(self) -> None:
self.__instrument_library_dialog.show()
self.__instrument_library_dialog.activateWindow()
2019-06-02 16:05:31 +02:00
def __showSettingsDialog(self) -> None:
self.__settings_dialog.show()
self.__settings_dialog.activateWindow()
def __profileAudioThread(self) -> None:
self.__audio_thread_profiler.show()
self.__audio_thread_profiler.raise_()
self.__audio_thread_profiler.activateWindow()
def __dumpAudioProc(self) -> None:
self.process.event_loop.create_task(self.audioproc_client.dump())
2018-12-23 18:38:51 +01:00
# pylint: disable=line-too-long
# def onPlayerStatus(self, player_state: audioproc.PlayerState):
# if pipeline_disabled:
# dialog = QtWidgets.QMessageBox(self)
# dialog.setIcon(QtWidgets.QMessageBox.Critical)
# dialog.setWindowTitle("noisicaa - Crash")
# dialog.setText(
# "The audio pipeline has been disabled, because it is repeatedly crashing.")
# quit_button = dialog.addButton("Quit", QtWidgets.QMessageBox.DestructiveRole)
# undo_and_restart_button = dialog.addButton(
# "Undo last command and restart pipeline", QtWidgets.QMessageBox.ActionRole)
# restart_button = dialog.addButton("Restart pipeline", QtWidgets.QMessageBox.AcceptRole)
# dialog.setDefaultButton(restart_button)
# dialog.finished.connect(lambda _: self.call_async(
# self.onPipelineDisabledDialogFinished(
# dialog, quit_button, undo_and_restart_button, restart_button)))
# dialog.show()
# async def onPipelineDisabledDialogFinished(
# self, dialog: QtWidgets.QMessageBox, quit_button: QtWidgets.QAbstractButton,
# undo_and_restart_button: QtWidgets.QAbstractButton,
# restart_button: QtWidgets.QAbstractButton) -> None:
# if dialog.clickedButton() == quit_button:
# self.app.quit()
# elif dialog.clickedButton() == restart_button:
# await self.project_client.restart_player_pipeline(self.__player_id)
# elif dialog.clickedButton() == undo_and_restart_button:
# await self.project_client.undo()
# await self.project_client.restart_player_pipeline(self.__player_id)
2018-12-23 18:38:51 +01:00
# pylint: enable=line-too-long
2016-07-17 00:17:25 +02:00
def setClipboardContent(self, content: Any) -> None:
logger.info(
"Setting clipboard contents to: %s", pprint.pformat(content))
self.__clipboard = content
def clipboardContent(self) -> Any:
return self.__clipboard
def crashWithMessage(self, title: str, msg: str) -> None:
logger.error('%s: %s', title, msg)
try:
errorbox = QtWidgets.QMessageBox()
errorbox.setWindowTitle("noisicaä crashed")
errorbox.setText(title)
errorbox.setInformativeText(msg)
errorbox.setIcon(QtWidgets.QMessageBox.Critical)
errorbox.addButton("Exit", QtWidgets.QMessageBox.AcceptRole)
errorbox.exec_()
except: # pylint: disable=bare-except
logger.error(
"Failed to show crash dialog: %s", traceback.format_exc())
sys.stdout.flush()
sys.stderr.flush()
os._exit(EXIT_EXCEPTION)