Add PipelineGraphMonitor to better debug audio graph changes.

looper
Ben Niemann 2016-08-21 16:06:59 +02:00
parent b63f1061dd
commit cfaffc2a0a
11 changed files with 377 additions and 9 deletions

View File

@ -10,3 +10,8 @@ from .audio_stream import (
from .data import (
FrameData,
)
from .mutations import (
Mutation,
AddNode, RemoveNode,
ConnectPorts, DisconnectPorts,
)

View File

@ -190,7 +190,7 @@ class AudioProcProcessMixin(object):
for port in node.inputs.values():
for upstream_port in port.inputs:
mutation = mutations.ConnectPorts(
port, upstream_port)
upstream_port, port)
session.publish_mutation(mutation)
return session.id

View File

@ -236,6 +236,10 @@ class ProjectClientMixin(object):
'CREATE_PLAYER', self._session_id,
self.server.address, sheet_id)
async def get_player_audioproc_address(self, player_id):
return await self._stub.call(
'GET_PLAYER_AUDIOPROC_ADDRESS', self._session_id, player_id)
async def delete_player(self, player_id):
return await self._stub.call(
'DELETE_PLAYER', self._session_id, player_id)

View File

@ -97,6 +97,9 @@ class ProjectProcessMixin(object):
'CREATE_PLAYER', self.handle_create_player)
self.server.add_command_handler(
'DELETE_PLAYER', self.handle_delete_player)
self.server.add_command_handler(
'GET_PLAYER_AUDIOPROC_ADDRESS',
self.handle_get_player_audioproc_address)
self.server.add_command_handler(
'PLAYER_START', self.handle_player_start)
self.server.add_command_handler(
@ -294,6 +297,12 @@ class ProjectProcessMixin(object):
return p.id, p.proxy_address
async def handle_get_player_audioproc_address(
self, session_id, player_id):
session = self.get_session(session_id)
p = session.players[player_id]
return p.audioproc_address
async def handle_delete_player(self, session_id, player_id):
session = self.get_session(session_id)
p = session.players[player_id]

View File

@ -263,13 +263,11 @@ class RemovePipelineGraphConnection(commands.Command):
def run(self, sheet):
assert isinstance(sheet, Sheet)
for idx, connection in enumerate(sheet.pipeline_graph_connections):
if connection.id == self.connection_id:
break
else:
raise ValueError("Connection %s not found" % self.connection_id)
root = sheet.root
connection = root.get_object(self.connection_id)
assert connection.is_child_of(sheet)
del sheet.pipeline_graph_connections[idx]
sheet.remove_pipeline_graph_connection(connection)
commands.Command.register_command(RemovePipelineGraphConnection)

View File

@ -25,6 +25,7 @@ from ..instr import library
from . import project_registry
from . import pipeline_perf_monitor
from . import pipeline_graph_monitor
logger = logging.getLogger('ui.editor_app')
@ -219,6 +220,7 @@ class EditorApp(BaseEditorApp):
self._old_excepthook = None
self.win = None
self.pipeline_perf_monitor = None
self.pipeline_graph_monitor = None
async def setup(self):
logger.info("Installing custom excepthook.")
@ -233,11 +235,16 @@ class EditorApp(BaseEditorApp):
logger.info("Creating PipelinePerfMonitor.")
self.pipeline_perf_monitor = pipeline_perf_monitor.PipelinePerfMonitor(self)
logger.info("Creating PipelineGraphMonitor.")
self.pipeline_graph_monitor = pipeline_graph_monitor.PipelineGraphMonitor(self)
logger.info("Creating EditorWindow.")
self.win = EditorWindow(self)
await self.win.setup()
self.win.show()
self.pipeline_graph_monitor.addWindow(self.win)
if self.paths:
logger.info("Starting with projects from cmdline.")
for path in self.paths:
@ -270,6 +277,10 @@ class EditorApp(BaseEditorApp):
self.pipeline_perf_monitor.storeState()
self.pipeline_perf_monitor = None
if self.pipeline_graph_monitor is not None:
self.pipeline_graph_monitor.storeState()
self.pipeline_graph_monitor = None
if self.win is not None:
await self.win.cleanup()
self.win = None

View File

@ -76,6 +76,8 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
currentSheetChanged = pyqtSignal(object)
currentTrackChanged = pyqtSignal(object)
projectListChanged = pyqtSignal()
def __init__(self, app):
super().__init__(app=app)
@ -271,6 +273,15 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
self.app.pipeline_perf_monitor.visibilityChanged.connect(
self._show_pipeline_perf_monitor_action.setChecked)
self._show_pipeline_graph_monitor_action = QAction(
"Pipeline Graph Monitor", self,
checkable=True,
checked=self.app.pipeline_graph_monitor.isVisible())
self._show_pipeline_graph_monitor_action.toggled.connect(
self.app.pipeline_graph_monitor.setVisible)
self.app.pipeline_graph_monitor.visibilityChanged.connect(
self._show_pipeline_graph_monitor_action.setChecked)
def createMenus(self):
menu_bar = self.menuBar()
@ -308,6 +319,8 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
self._dev_menu.addAction(self.app.show_edit_areas_action)
self._dev_menu.addAction(
self._show_pipeline_perf_monitor_action)
self._dev_menu.addAction(
self._show_pipeline_graph_monitor_action)
menu_bar.addSeparator()
@ -442,6 +455,8 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
self._add_score_track_action.setEnabled(True)
self._main_area.setCurrentIndex(0)
self.projectListChanged.emit()
async def removeProjectView(self, project_connection):
for idx in range(self._project_tabs.count()):
view = self._project_tabs.widget(idx)
@ -455,6 +470,7 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
self._project_tabs.count() > 0)
await view.cleanup()
self.projectListChanged.emit()
break
else:
raise ValueError("No view for project found.")
@ -480,6 +496,10 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow):
def getCurrentProjectView(self):
return self._project_tabs.currentWidget()
def listProjectViews(self):
for idx in range(self._project_tabs.count()):
yield self._project_tabs.widget(idx)
def getCurrentProject(self):
view = self._project_tabs.currentWidget()
return view.project

View File

@ -0,0 +1,308 @@
#!/usr/bin/python
import logging
import math
import random
import time
import toposort
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import audioproc
from noisicaa import core
from noisicaa.core import ipc
from . import ui_base
logger = logging.getLogger(__name__)
class AudioProcClientImpl(object):
def __init__(self, monitor):
super().__init__()
self.event_loop = monitor.event_loop
self.monitor = monitor
self.server = ipc.Server(self.event_loop, 'audioproc_monitor')
async def setup(self):
await self.server.setup()
async def cleanup(self):
await self.server.cleanup()
class AudioProcClient(
audioproc.AudioProcClientMixin, AudioProcClientImpl):
def handle_pipeline_mutation(self, mutation):
self.monitor.onPipelineMutation(mutation)
def handle_pipeline_status(self, status):
self.monitor.onPipelineStatus(status)
class Port(QtWidgets.QGraphicsRectItem):
def __init__(self, parent, node_id, port_name, port_direction):
super().__init__(parent)
self.node_id = node_id
self.port_name = port_name
self.port_direction = port_direction
self.setRect(0, 0, 45, 15)
self.setBrush(Qt.white)
if self.port_direction == 'input':
self.dot_pos = QtCore.QPoint(7, 7)
else:
self.dot_pos = QtCore.QPoint(45-7, 7)
dot = QtWidgets.QGraphicsRectItem(self)
dot.setRect(-1, -1, 3, 3)
dot.setPos(self.dot_pos)
dot.setBrush(Qt.black)
class Node(QtWidgets.QGraphicsRectItem):
def __init__(self, node_id, desc):
super().__init__()
self.node_id = node_id
self.desc = desc
self.setFlag(self.ItemSendsGeometryChanges, True)
self.setRect(0, 0, 100, 60)
if self.desc.is_system:
self.setBrush(QtGui.QBrush(QtGui.QColor(200, 200, 255)))
else:
self.setBrush(Qt.white)
self.ports = {}
self.connections = set()
label = QtWidgets.QGraphicsTextItem(self)
label.setPos(2, 2)
label.setPlainText(self.desc.name)
in_y = 25
out_y = 25
for port_name, port_direction, port_type in self.desc.ports:
if port_direction == 'input':
x = -5
y = in_y
in_y += 20
elif port_direction == 'output':
x = 105-45
y = out_y
out_y += 20
port = Port(self, self.node_id, port_name, port_direction)
port.setPos(x, y)
self.ports[port_name] = port
def itemChange(self, change, value):
if change == self.ItemPositionHasChanged:
for connection in self.connections:
connection.update()
return super().itemChange(change, value)
class Connection(QtWidgets.QGraphicsLineItem):
def __init__(self, node1, port1, node2, port2):
super().__init__()
self.node1 = node1
self.port1 = port1
self.node2 = node2
self.port2 = port2
self.update()
def update(self):
pos1 = self.port1.mapToScene(self.port1.dot_pos)
pos2 = self.port2.mapToScene(self.port2.dot_pos)
self.setLine(QtCore.QLineF(pos1, pos2))
class PipelineGraphMonitor(ui_base.CommonMixin, QtWidgets.QMainWindow):
visibilityChanged = QtCore.pyqtSignal(bool)
def __init__(self, app):
super().__init__(app=app)
self.setWindowTitle("noisicaä - Pipeline Graph Monitor")
self.resize(600, 300)
self.__windows = []
self.__current_sheet_view = None
self.__audioproc_client = None
self.sheet_selector = QtWidgets.QComboBox()
self.sheet_selector.currentIndexChanged.connect(
self.onSheetChanged)
self.zoomInAction = QtWidgets.QAction(
QtGui.QIcon.fromTheme('zoom-in'),
"Zoom In",
self, triggered=self.onZoomIn)
self.zoomOutAction = QtWidgets.QAction(
QtGui.QIcon.fromTheme('zoom-out'),
"Zoom Out",
self, triggered=self.onZoomOut)
self.toolbar = QtWidgets.QToolBar()
self.toolbar.addWidget(self.sheet_selector)
self.toolbar.addSeparator()
self.toolbar.addAction(self.zoomInAction)
self.toolbar.addAction(self.zoomOutAction)
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
self.nodes = {}
self.connections = {}
self.scene = QtWidgets.QGraphicsScene()
self.graph_view = QtWidgets.QGraphicsView(self.scene, self)
self.graph_view.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.graph_view.setDragMode(
QtWidgets.QGraphicsView.ScrollHandDrag)
self.setCentralWidget(self.graph_view)
self.setVisible(
int(self.app.settings.value(
'dialog/pipeline_graph_monitor/visible', False)))
self.restoreGeometry(
self.app.settings.value(
'dialog/pipeline_graph_monitor/geometry', b''))
def storeState(self):
s = self.app.settings
s.beginGroup('dialog/pipeline_graph_monitor')
s.setValue('visible', int(self.isVisible()))
s.setValue('geometry', self.saveGeometry())
s.endGroup()
def showEvent(self, event):
self.visibilityChanged.emit(True)
super().showEvent(event)
def hideEvent(self, event):
self.visibilityChanged.emit(False)
super().hideEvent(event)
def onZoomIn(self):
pass
def onZoomOut(self):
pass
def onProjectListChanged(self):
self.sheet_selector.clear()
for win in self.__windows:
for project_view in win.listProjectViews():
for sheet_view in project_view.sheetViews:
self.sheet_selector.addItem(
'%s: %s' % (
project_view.project_connection.name,
sheet_view.sheet.name),
sheet_view)
if sheet_view is self.__current_sheet_view:
self.sheet_selector.setCurrentIndex(
self.sheet_selector.count())
def onSheetChanged(self, index):
sheet_view = self.sheet_selector.itemData(index)
if sheet_view is self.__current_sheet_view:
return
self.call_async(self.changeSheet(sheet_view))
async def changeSheet(self, sheet_view):
if self.__current_sheet_view is not None:
self.scene.clear()
self.__current_sheet_view = None
if self.__audioproc_client is not None:
await self.__audioproc_client.disconnect(shutdown=False)
await self.__audioproc_client.cleanup()
self.__audioproc_client = None
if sheet_view is not None:
self.__audioproc_client = AudioProcClient(self)
await self.__audioproc_client.setup()
await self.__audioproc_client.connect(
sheet_view.player_audioproc_address)
self.__current_sheet_view = sheet_view
def addWindow(self, win):
self.__windows.append(win)
win.projectListChanged.connect(self.onProjectListChanged)
self.onProjectListChanged()
def onPipelineMutation(self, mutation):
if isinstance(mutation, audioproc.AddNode):
node = Node(mutation.id, mutation.desc)
node.setPos(random.randint(-200, 200), random.randint(-200, 200))
self.scene.addItem(node)
self.nodes[mutation.id] = node
elif isinstance(mutation, audioproc.RemoveNode):
node = self.nodes[mutation.id]
self.scene.removeItem(node)
del self.nodes[mutation.id]
elif isinstance(mutation, audioproc.ConnectPorts):
connection_id = '%s:%s-%s-%s' % (
mutation.node1, mutation.port1,
mutation.node2, mutation.port2)
node1 = self.nodes[mutation.node1]
node2 = self.nodes[mutation.node2]
port1 = node1.ports[mutation.port1]
port2 = node2.ports[mutation.port2]
connection = Connection(node1, port1, node2, port2)
self.scene.addItem(connection)
self.connections[connection_id] = connection
node1.connections.add(connection)
node2.connections.add(connection)
elif isinstance(mutation, audioproc.DisconnectPorts):
connection_id = '%s:%s-%s-%s' % (
mutation.node1, mutation.port1,
mutation.node2, mutation.port2)
connection = self.connections[connection_id]
self.scene.removeItem(connection)
del self.connections[connection_id]
connection.node1.connections.remove(connection)
connection.node2.connections.remove(connection)
else:
logger.warning("Unknown mutation received: %s", mutation)
graph = {}
for node in self.nodes.values():
graph[node.node_id] = set()
for connection in self.connections.values():
graph[connection.node2.node_id].add(connection.node1.node_id)
try:
sorted_nodes = list(toposort.toposort(graph))
except ValueError as exc:
logger.error("Sorting audio proc graph failed: %s", exc)
else:
x = 0
for layer in sorted_nodes:
y = 0
for node_id in sorted(layer):
node = self.nodes[node_id]
node.setPos(x, y)
y += 150
x += 200
def onPipelineStatus(self, status):
logger.info("onPipelineStatus(%s)", status)

View File

@ -179,7 +179,6 @@ class NodeItemImpl(QtWidgets.QGraphicsRectItem):
self._graph_pos_listener = None
def setHighlighted(self, highlighted):
logger.info("%s setHighlighted(%s)", self, highlighted)
if highlighted:
self.setBrush(QtGui.QColor(240, 240, 255))
else:

View File

@ -3,6 +3,8 @@
import logging
import os.path
from PyQt5 import QtCore
from noisicaa import music
from . import model
@ -45,8 +47,12 @@ class Project(object):
self.client = None
class ProjectRegistry(object):
class ProjectRegistry(QtCore.QObject):
projectListChanged = QtCore.pyqtSignal()
def __init__(self, event_loop, process_manager):
super().__init__()
self.event_loop = event_loop
self.process_manager = process_manager
self.projects = {}
@ -55,18 +61,22 @@ class ProjectRegistry(object):
project = Project(path, self.event_loop, self.process_manager)
await project.open()
self.projects[path] = project
self.projectListChanged.emit()
return project
async def create_project(self, path):
project = Project(path, self.event_loop, self.process_manager)
await project.create()
self.projects[path] = project
self.projectListChanged.emit()
return project
async def close_project(self, project):
await project.close()
del self.projects[project.path]
self.projectListChanged.emit()
async def close_all(self):
for project in list(self.projects.values()):
await self.close_project(project)
self.projectListChanged.emit()

View File

@ -1343,11 +1343,15 @@ class SheetViewImpl(QGraphicsView):
self._player_node_id = None
self._player_status_listener = None
self.player_audioproc_address = None
async def setup(self):
self._player_id, self._player_stream_address = await self.project_client.create_player(self._sheet.id)
self._player_status_listener = self.project_client.add_player_status_listener(
self._player_id, self.onPlayerStatus)
self.player_audioproc_address = await self.project_client.get_player_audioproc_address(self._player_id)
self._player_node_id = await self.audioproc_client.add_node(
'ipc',
address=self._player_stream_address,