diff --git a/noisicaa/audioproc/audioproc_client.py b/noisicaa/audioproc/audioproc_client.py index b3e44a00..bfd5937d 100644 --- a/noisicaa/audioproc/audioproc_client.py +++ b/noisicaa/audioproc/audioproc_client.py @@ -63,6 +63,10 @@ class AudioProcClientMixin(object): 'DISCONNECT_PORTS', self._session_id, node1_id, port1_name, node2_id, port2_name) + async def set_backend(self, name, **args): + return await self._stub.call( + 'SET_BACKEND', self._session_id, name, args) + def handle_pipeline_mutation(self, mutation): logger.info("Mutation received: %s" % mutation) diff --git a/noisicaa/audioproc/audioproc_process.py b/noisicaa/audioproc/audioproc_process.py index af67b1fb..b7df1aef 100644 --- a/noisicaa/audioproc/audioproc_process.py +++ b/noisicaa/audioproc/audioproc_process.py @@ -68,6 +68,10 @@ class Session(object): class AudioProcProcessMixin(object): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.backend = None + async def setup(self): await super().setup() @@ -88,6 +92,8 @@ class AudioProcProcessMixin(object): 'CONNECT_PORTS', self.handle_connect_ports) self.server.add_command_handler( 'DISCONNECT_PORTS', self.handle_disconnect_ports) + self.server.add_command_handler( + 'SET_BACKEND', self.handle_set_backend) self.node_db = node_db.NodeDB() self.node_db.add(scale.Scale) @@ -99,8 +105,7 @@ class AudioProcProcessMixin(object): self.pipeline = pipeline.Pipeline() self.pipeline.utilization_callback = self.utilization_callback - self.backend = backend.NullBackend() - self.pipeline.set_backend(self.backend) + self.backend = None self.audiosink = backend.AudioSinkNode() self.audiosink.setup() @@ -223,6 +228,20 @@ class AudioProcProcessMixin(object): mutations.DisconnectPorts( node1.outputs[port1_name], node2.inputs[port2_name])) + def handle_set_backend(self, session_id, name, args): + self.get_session(session_id) + + if name == 'pyaudio': + be = backend.PyAudioBackend(**args) + elif name == 'null': + be = backend.NullBackend(**args) + elif name is None: + be = None + else: + raise ValueError("Invalid backend name %s" % name) + + self.pipeline.set_backend(be) + class AudioProcProcess(AudioProcProcessMixin, core.ProcessImpl): pass diff --git a/noisicaa/audioproc/backend.py b/noisicaa/audioproc/backend.py index 62fde12a..3179879c 100644 --- a/noisicaa/audioproc/backend.py +++ b/noisicaa/audioproc/backend.py @@ -26,7 +26,7 @@ class AudioSinkNode(Node): desc.is_system = True def __init__(self): - super().__init__() + super().__init__(id='sink') self._input = AudioInputPort('in') self.add_input(self._input) diff --git a/noisicaa/audioproc/node.py b/noisicaa/audioproc/node.py index 4ad079f5..d7e1955d 100644 --- a/noisicaa/audioproc/node.py +++ b/noisicaa/audioproc/node.py @@ -12,8 +12,8 @@ logger = logging.getLogger(__name__) class Node(object): desc = None - def __init__(self, name=None): - self.id = uuid.uuid4().hex + def __init__(self, name=None, id=None): + self.id = id or uuid.uuid4().hex self.pipeline = None self._name = name or type(self).__name__ self.inputs = {} diff --git a/noisicaa/audioproc/pipeline.py b/noisicaa/audioproc/pipeline.py index 86602043..31a7a7a6 100644 --- a/noisicaa/audioproc/pipeline.py +++ b/noisicaa/audioproc/pipeline.py @@ -102,14 +102,15 @@ class Pipeline(object): def mainloop(self): try: - logger.info("Setting up backend...") - self._backend.setup() - logger.info("Starting mainloop...") self._started.set() timepos = 0 while not self._stopping.is_set(): with self.reader_lock(): + if self._backend is None: + time.sleep(0.1) + continue + t0 = time.time() self._backend.wait() @@ -136,9 +137,6 @@ class Pipeline(object): for node in reversed(self.sorted_nodes): node.cleanup() - logger.info("Cleaning up backend...") - self._backend.cleanup() - @property def sorted_nodes(self): graph = dict((node, set(node.parent_nodes)) @@ -167,7 +165,18 @@ class Pipeline(object): self._nodes.remove(node) def set_backend(self, backend): - self._backend = backend + with self.writer_lock(): + if self._backend is not None: + logger.info( + "Clean up backend %s", type(self._backend).__name__) + self._backend.cleanup() + self._backend = None + + if backend is not None: + logger.info( + "Set up backend %s", type(backend).__name__) + backend.setup() + self._backend = backend @property def backend(self): diff --git a/noisicaa/core/model_base.py b/noisicaa/core/model_base.py index a7e67acb..4ca7815e 100644 --- a/noisicaa/core/model_base.py +++ b/noisicaa/core/model_base.py @@ -202,7 +202,6 @@ class ObjectPropertyBase(PropertyBase): class ObjectProperty(ObjectPropertyBase): def __set__(self, instance, value): - logger.info("%s.%s=%s", type(instance), self.name, value) if value is not None and not isinstance(value, self.cls): raise TypeError( "Expected %s, got %s" % ( diff --git a/noisicaa/ui/editor_app.py b/noisicaa/ui/editor_app.py index 47864a33..6f6a0833 100644 --- a/noisicaa/ui/editor_app.py +++ b/noisicaa/ui/editor_app.py @@ -129,6 +129,9 @@ class BaseEditorApp(QApplication): await self.audioproc_client.setup() await self.audioproc_client.connect(self.audioproc_process) + await self.audioproc_client.set_backend( + self.settings.value('audio/backend', 'pyaudio')) + async def cleanup(self): logger.info("Cleaning up.") diff --git a/noisicaa/ui/settings.py b/noisicaa/ui/settings.py index 0a56e590..26782567 100644 --- a/noisicaa/ui/settings.py +++ b/noisicaa/ui/settings.py @@ -4,44 +4,36 @@ # message "Access to a protected member .. of a client class" # pylint: disable=W0212 +import functools import os.path -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import ( - QComboBox, - QDialog, - QHBoxLayout, - QLabel, - QPushButton, - QStyleFactory, - QTabWidget, - QVBoxLayout, - QWidget, -) +from PyQt5 import QtGui +from PyQt5 import QtWidgets from ..constants import DATA_DIR from . import ui_base -class SettingsDialog(ui_base.CommonMixin, QDialog): +class SettingsDialog(ui_base.CommonMixin, QtWidgets.QDialog): def __init__(self, app, parent): super().__init__(app=app, parent=parent) self.setWindowTitle("noisicaƤ - Settings") + self.resize(600, 300) - self.tabs = QTabWidget(self) + self.tabs = QtWidgets.QTabWidget(self) for cls in (AppearancePage, AudioPage): page = cls(self.app) self.tabs.addTab(page, page.getIcon(), page.title) - close = QPushButton("Close") + close = QtWidgets.QPushButton("Close") close.clicked.connect(self.close) - buttons = QHBoxLayout() + buttons = QtWidgets.QHBoxLayout() buttons.addStretch(1) buttons.addWidget(close) - layout = QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(self.tabs, 1) layout.addLayout(buttons) @@ -65,11 +57,11 @@ class SettingsDialog(ui_base.CommonMixin, QDialog): s.endGroup() -class Page(ui_base.CommonMixin, QWidget): +class Page(ui_base.CommonMixin, QtWidgets.QWidget): def __init__(self, app): super().__init__(app=app) - layout = QVBoxLayout() + layout = QtWidgets.QVBoxLayout() self.createOptions(layout) @@ -80,25 +72,25 @@ class Page(ui_base.CommonMixin, QWidget): class AppearancePage(Page): def __init__(self, app): self.title = "Appearance" - self._qt_styles = sorted(QStyleFactory.keys()) + self._qt_styles = sorted(QtWidgets.QStyleFactory.keys()) super().__init__(app) def getIcon(self): path = os.path.join(DATA_DIR, 'icons', 'settings_appearance.png') - return QIcon(path) + return QtGui.QIcon(path) def createOptions(self, layout): self.createQtStyle(layout) def createQtStyle(self, parent): - layout = QHBoxLayout() + layout = QtWidgets.QHBoxLayout() parent.addLayout(layout) - label = QLabel("Qt Style:") + label = QtWidgets.QLabel("Qt Style:") layout.addWidget(label) - combo = QComboBox() + combo = QtWidgets.QComboBox() layout.addWidget(combo) current = self.app.settings.value( @@ -113,7 +105,7 @@ class AppearancePage(Page): def qtStyleChanged(self, index): style_name = self._qt_styles[index] - style = QStyleFactory.create(style_name) + style = QtWidgets.QStyleFactory.create(style_name) self.app.setStyle(style) self.app.settings.setValue('appearance/qtStyle', style_name) @@ -122,11 +114,57 @@ class AppearancePage(Page): class AudioPage(Page): def __init__(self, app): self.title = "Audio" + self._backends = ['pyaudio', 'null'] super().__init__(app) def getIcon(self): path = os.path.join(DATA_DIR, 'icons', 'settings_audio.png') - return QIcon(path) + return QtGui.QIcon(path) def createOptions(self, layout): - pass + backend_layout = QtWidgets.QHBoxLayout() + layout.addLayout(backend_layout) + + label = QtWidgets.QLabel("Backend:") + backend_layout.addWidget(label) + + combo = QtWidgets.QComboBox() + backend_layout.addWidget(combo, stretch=1) + + current = self.app.settings.value('audio/backend', 'pyaudio') + for index, backend in enumerate(self._backends): + combo.addItem(backend) + if backend == current: + combo.setCurrentIndex(index) + combo.currentIndexChanged.connect(self.backendChanged) + + layout.addStretch() + + buttons_layout = QtWidgets.QHBoxLayout() + layout.addLayout(buttons_layout) + buttons_layout.addStretch() + + test_button = QtWidgets.QPushButton("Test") + buttons_layout.addWidget(test_button) + + test_button.clicked.connect(self.testBackend) + + def backendChanged(self, index): + backend = self._backends[index] + + self.call_async( + self.app.audioproc_client.set_backend(backend), + callback=functools.partial( + self._set_backend_done, backend=backend)) + + def _set_backend_done(self, result, backend): + self.app.settings.setValue('audio/backend', backend) + + def testBackend(self): + self.call_async(self._testBackendAsync()) + + async def _testBackendAsync(self): + node = await self.app.audioproc_client.add_node( + 'wavfile', path='/usr/share/sounds/purple/send.wav') + await self.app.audioproc_client.connect_ports( + node, 'out', 'sink', 'in')