diff --git a/noisicaa/music/instrument.py b/noisicaa/music/instrument.py index 70d0cc3d..c7fa06b4 100644 --- a/noisicaa/music/instrument.py +++ b/noisicaa/music/instrument.py @@ -19,6 +19,10 @@ class Instrument(model.Instrument, state.StateBase): def track(self): return self.parent + @property + def sheet(self): + return self.track.sheet + @property def project(self): return self.track.project @@ -54,8 +58,7 @@ class SoundFontInstrument(model.SoundFontInstrument, Instrument): return (self.path, self.bank, self.preset) == (other.path, other.bank, other.preset) def add_to_pipeline(self): - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.AddNode( 'fluidsynth', self.pipeline_node_id, self.name, soundfont_path=self.path, @@ -63,8 +66,7 @@ class SoundFontInstrument(model.SoundFontInstrument, Instrument): preset=self.preset)) def remove_from_pipeline(self): - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.RemoveNode(self.pipeline_node_id)) state.StateBase.register_class(SoundFontInstrument) diff --git a/noisicaa/music/model.py b/noisicaa/music/model.py index d7e4ae8e..a5f08031 100644 --- a/noisicaa/music/model.py +++ b/noisicaa/music/model.py @@ -12,7 +12,7 @@ from . import time_signature class Instrument(core.ObjectBase): name = core.Property(str) - library_id = core.Property(str) + library_id = core.Property(str, allow_none=True) class SoundFontInstrument(Instrument): diff --git a/noisicaa/music/player.py b/noisicaa/music/player.py new file mode 100644 index 00000000..dd895cfb --- /dev/null +++ b/noisicaa/music/player.py @@ -0,0 +1,124 @@ +#!/usr/bin/python3 + +import functools +import asyncio +import logging +import threading +import time +import uuid + +from noisicaa import core +from noisicaa.core import ipc +from noisicaa import audioproc + +from . import project +from . import mutations +from . import commands + +logger = logging.getLogger(__name__) + + +class AudioProcClientImpl(object): + def __init__(self, event_loop, server): + super().__init__() + self.event_loop = event_loop + self.server = server + + async def setup(self): + pass + + async def cleanup(self): + pass + +class AudioProcClient( + audioproc.AudioProcClientMixin, AudioProcClientImpl): + pass + + +class Player(object): + def __init__(self, sheet, manager, event_loop): + self.sheet = sheet + self.manager = manager + self.event_loop = event_loop + + self.id = uuid.uuid4().hex + self.server = ipc.Server(self.event_loop, 'player') + + self.setup_complete = False + + self.audioproc_address = None + self.audioproc_client = None + self.audiostream_address = None + + self.mutation_listener = None + self.pending_pipeline_mutations = None + + async def setup(self): + await self.server.setup() + + self.audioproc_address = await self.manager.call( + 'CREATE_AUDIOPROC_PROCESS', 'player') + self.audioproc_client = AudioProcClient( + self.event_loop, self.server) + await self.audioproc_client.setup() + await self.audioproc_client.connect(self.audioproc_address) + self.audiostream_address = await self.audioproc_client.set_backend('ipc') + + self.pending_pipeline_mutations = [] + self.mutation_listener = self.sheet.listeners.add( + 'pipeline_mutations', self.handle_pipeline_mutation) + + self.sheet.add_to_pipeline() + pipeline_mutations = self.pending_pipeline_mutations[:] + self.pending_pipeline_mutations = None + + for mutation in pipeline_mutations: + await self.publish_pipeline_mutation(mutation) + + await self.audioproc_client.dump() + + async def cleanup(self): + if self.mutation_listener is not None: + self.mutation_listener.remove() + self.mutation_listener = None + + if self.audioproc_client is not None: + await self.audioproc_client.disconnect(shutdown=True) + await self.audioproc_client.cleanup() + self.audioproc_client = None + self.audioproc_address = None + self.audiostream_address = None + + await self.server.cleanup() + + def handle_pipeline_mutation(self, mutation): + if self.pending_pipeline_mutations is not None: + self.pending_pipeline_mutations.append(mutation) + else: + self.event_loop.create_task( + self.publish_pipeline_mutation(mutation)) + + async def publish_pipeline_mutation(self, mutation): + if self.audioproc_client is None: + return + + if isinstance(mutation, mutations.AddNode): + await self.audioproc_client.add_node( + mutation.node_type, id=mutation.node_id, + name=mutation.node_name, **mutation.args) + + elif isinstance(mutation, mutations.RemoveNode): + await self.audioproc_client.remove_node(mutation.node_id) + + elif isinstance(mutation, mutations.ConnectPorts): + await self.audioproc_client.connect_ports( + mutation.src_node, mutation.src_port, + mutation.dest_node, mutation.dest_port) + + elif isinstance(mutation, mutations.DisconnectPorts): + await self.audioproc_client.disconnect_ports( + mutation.src_node, mutation.src_port, + mutation.dest_node, mutation.dest_port) + + else: + raise ValueError(type(mutation)) diff --git a/noisicaa/music/project.py b/noisicaa/music/project.py index b3f2aa77..9e7b2033 100644 --- a/noisicaa/music/project.py +++ b/noisicaa/music/project.py @@ -288,6 +288,8 @@ commands.Command.register_command(RemoveMeasure) class Sheet(model.Sheet, state.StateBase): def __init__(self, name=None, num_tracks=1, state=None): super().__init__(state) + self.listeners = core.CallbackRegistry() + if state is None: self.name = name @@ -338,20 +340,20 @@ class Sheet(model.Sheet, state.StateBase): while len(track.measures) < max_length: track.append_measure() + def handle_pipeline_mutation(self, mutation): + self.listeners.call('pipeline_mutations', mutation) + @property def main_mixer_name(self): return '%s-sheet-mixer' % self.id def add_to_pipeline(self): - self.project.listeners.call( - 'pipeline_mutations', + self.handle_pipeline_mutation( mutations.AddNode( 'passthru', self.main_mixer_name, 'sheet-mixer')) - self.project.listeners.call( - 'pipeline_mutations', + self.handle_pipeline_mutation( mutations.ConnectPorts( - self.main_mixer_name, 'out', - self.project.main_mixer_name, 'in')) + self.main_mixer_name, 'out', 'sink', 'in')) for track in self.tracks: track.add_to_pipeline() @@ -360,13 +362,10 @@ class Sheet(model.Sheet, state.StateBase): for track in self.tracks: track.remove_from_pipeline() - self.project.listeners.call( - 'pipeline_mutations', + self.handle_pipeline_mutation( mutations.DisconnectPorts( - self.main_mixer_name, 'out', - self.project.main_mixer_name, 'in')) - self.project.listeners.call( - 'pipeline_mutations', + self.main_mixer_name, 'out', 'sink', 'in')) + self.handle_pipeline_mutation( mutations.RemoveNode(self.main_mixer_name)) state.StateBase.register_class(Sheet) @@ -439,23 +438,6 @@ class BaseProject(model.Project, state.RootMixin, state.StateBase): def handle_mutation(self, mutation): self.listeners.call('project_mutations', mutation) - @property - def main_mixer_name(self): - return '%s-main-mixer' % self.id - - def add_to_pipeline(self): - self.listeners.call( - 'pipeline_mutations', - mutations.AddNode( - 'passthru', self.main_mixer_name, 'main-mixer')) - self.listeners.call( - 'pipeline_mutations', - mutations.ConnectPorts( - self.main_mixer_name, 'out', 'sink', 'in')) - - for sheet in self.sheets: - sheet.add_to_pipeline() - @classmethod def make_demo(cls): project = cls(num_sheets=0) diff --git a/noisicaa/music/project_client.py b/noisicaa/music/project_client.py index 5bf34938..be6be345 100644 --- a/noisicaa/music/project_client.py +++ b/noisicaa/music/project_client.py @@ -59,7 +59,6 @@ class ProjectClientMixin(object): self._session_id = None self._object_map = {} self.project = None - self.audiostream_address = None self.cls_map = {} def __set_project(self, root_id): @@ -78,7 +77,7 @@ class ProjectClientMixin(object): assert self._stub is None self._stub = ipc.Stub(self.event_loop, address) await self._stub.connect() - self._session_id, self.audiostream_address, root_id = await self._stub.call( + self._session_id, root_id = await self._stub.call( 'START_SESSION', self.server.address) if root_id is not None: # Connected to a loaded project. @@ -208,5 +207,13 @@ class ProjectClientMixin(object): logger.info("Command %s completed with result=%r", command, result) return result + async def create_player(self, sheet_id): + return await self._stub.call( + 'CREATE_PLAYER', self._session_id, sheet_id) + + async def delete_player(self, player_id): + return await self._stub.call( + 'DELETE_PLAYER', self._session_id, player_id) + class ProjectClient(ProjectClientMixin, ProjectClientBase): pass diff --git a/noisicaa/music/project_process.py b/noisicaa/music/project_process.py index 465a96eb..e237d97e 100644 --- a/noisicaa/music/project_process.py +++ b/noisicaa/music/project_process.py @@ -14,6 +14,7 @@ from noisicaa import audioproc from . import project from . import mutations from . import commands +from . import player logger = logging.getLogger(__name__) @@ -26,9 +27,12 @@ class Session(object): self.event_loop = event_loop self.callback_stub = callback_stub self.id = uuid.uuid4().hex + self.players = {} - def cleanup(self): - pass + async def cleanup(self): + for p in self.players.values(): + await p.cleanup() + self.players.clear() async def publish_mutation(self, mutation): assert self.callback_stub.connected @@ -67,10 +71,6 @@ class ProjectProcessMixin(object): self.project = None self.sessions = {} self.pending_mutations = [] - self.pending_pipeline_mutations = [] - self.audioproc_address = None - self.audioproc_client = None - self.audiostream_address = None async def setup(self): await super().setup() @@ -88,21 +88,13 @@ class ProjectProcessMixin(object): self.server.add_command_handler('OPEN', self.handle_open) self.server.add_command_handler('CLOSE', self.handle_close) self.server.add_command_handler('COMMAND', self.handle_command) - - await self.createAudioProcProcess() - - async def createAudioProcProcess(self): - pass + self.server.add_command_handler( + 'CREATE_PLAYER', self.handle_create_player) + self.server.add_command_handler( + 'DELETE_PLAYER', self.handle_delete_player) async def cleanup(self): - logger.info("Cleaning up project process.") - - if self.audioproc_client is not None: - await self.audioproc_client.disconnect(shutdown=True) - await self.audioproc_client.cleanup() - self.audioproc_client = None - self.audioproc_address = None - self.audiostream_address = None + pass async def run(self): await self._shutting_down.wait() @@ -159,10 +151,6 @@ class ProjectProcessMixin(object): else: raise ValueError(mtype) - def handle_pipeline_mutation(self, mutation): - logger.info("Pipeline mutation: %s", mutation) - self.pending_pipeline_mutations.append(mutation) - async def publish_mutation(self, mutation): tasks = [] for session in self.sessions.values(): @@ -170,31 +158,6 @@ class ProjectProcessMixin(object): session.publish_mutation(mutation))) await asyncio.wait(tasks, loop=self.event_loop) - async def publish_pipeline_mutation(self, mutation): - if self.audioproc_client is None: - return - - if isinstance(mutation, mutations.AddNode): - await self.audioproc_client.add_node( - mutation.node_type, id=mutation.node_id, - name=mutation.node_name, **mutation.args) - - elif isinstance(mutation, mutations.RemoveNode): - await self.audioproc_client.remove_node(mutation.node_id) - - elif isinstance(mutation, mutations.ConnectPorts): - await self.audioproc_client.connect_ports( - mutation.src_node, mutation.src_port, - mutation.dest_node, mutation.dest_port) - - elif isinstance(mutation, mutations.DisconnectPorts): - await self.audioproc_client.disconnect_ports( - mutation.src_node, mutation.src_port, - mutation.dest_node, mutation.dest_port) - - else: - raise ValueError(type(mutation)) - async def handle_start_session(self, client_address): client_stub = ipc.Stub(self.event_loop, client_address) await client_stub.connect() @@ -203,12 +166,12 @@ class ProjectProcessMixin(object): if self.project is not None: for mutation in self.add_object_mutations(self.project): await session.publish_mutation(mutation) - return session.id, self.audiostream_address, self.project.id - return session.id, self.audiostream_address, None + return session.id, self.project.id + return session.id, None - def handle_end_session(self, session_id): + async def handle_end_session(self, session_id): session = self.get_session(session_id) - session.cleanup() + await session.cleanup() del self.sessions[session_id] def handle_shutdown(self): @@ -235,17 +198,6 @@ class ProjectProcessMixin(object): for mutation in self.add_object_mutations(self.project): await self.publish_mutation(mutation) - async def send_initial_pipeline_mutations(self): - assert not self.pending_pipeline_mutations - self.project.add_to_pipeline() - pipeline_mutations = self.pending_pipeline_mutations[:] - self.pending_pipeline_mutations.clear() - - for mutation in pipeline_mutations: - await self.publish_pipeline_mutation(mutation) - - await self.audioproc_client.dump() - async def handle_create(self, path): assert self.project is None self.project = project.Project() @@ -253,9 +205,6 @@ class ProjectProcessMixin(object): await self.send_initial_mutations() self.project.listeners.add( 'project_mutations', self.handle_project_mutation) - self.project.listeners.add( - 'pipeline_mutations', self.handle_pipeline_mutation) - await self.send_initial_pipeline_mutations() return self.project.id async def handle_create_inmemory(self): @@ -264,9 +213,6 @@ class ProjectProcessMixin(object): await self.send_initial_mutations() self.project.listeners.add( 'project_mutations', self.handle_project_mutation) - self.project.listeners.add( - 'pipeline_mutations', self.handle_pipeline_mutation) - await self.send_initial_pipeline_mutations() return self.project.id async def handle_open(self, path): @@ -276,9 +222,6 @@ class ProjectProcessMixin(object): await self.send_initial_mutations() self.project.listeners.add( 'project_mutations', self.handle_project_mutation) - self.project.listeners.add( - 'pipeline_mutations', self.handle_pipeline_mutation) - await self.send_initial_pipeline_mutations() return self.project.id def handle_close(self): @@ -298,30 +241,35 @@ class ProjectProcessMixin(object): # This block must be atomic, no 'awaits'! assert not self.pending_mutations - assert not self.pending_pipeline_mutations cmd = commands.Command.create(command, **kwargs) result = self.project.dispatch_command(target, cmd) mutations = self.pending_mutations[:] self.pending_mutations.clear() - pipeline_mutations = self.pending_pipeline_mutations[:] - self.pending_pipeline_mutations.clear() for mutation in mutations: await self.publish_mutation(mutation) - for mutation in pipeline_mutations: - await self.publish_pipeline_mutation(mutation) - return result + async def handle_create_player(self, session_id, sheet_id): + session = self.get_session(session_id) + assert self.project is not None -class ProjectProcess(ProjectProcessMixin, core.ProcessImpl): - async def createAudioProcProcess(self): - self.audioproc_address = await self.manager.call( - 'CREATE_AUDIOPROC_PROCESS', 'project-%s' % id(self)) - self.audioproc_client = AudioProcClient( - self.event_loop, self.server) - await self.audioproc_client.setup() - await self.audioproc_client.connect(self.audioproc_address) - self.audiostream_address = await self.audioproc_client.set_backend('ipc') + sheet = self.project.get_object(sheet_id) + p = player.Player(sheet, self.manager, self.event_loop) + await p.setup() + + session.players[p.id] = p + + return p.id, p.audiostream_address + + async def handle_delete_player(self, session_id, player_id): + session = self.get_session(session_id) + p = session.players[player_id] + await p.cleanup() + del session.players[player_id] + + +class ProjectProcess(ProjectProcessMixin, core.ProcessImpl): + pass diff --git a/noisicaa/music/state.py b/noisicaa/music/state.py index 528bda1f..16f7f39f 100644 --- a/noisicaa/music/state.py +++ b/noisicaa/music/state.py @@ -38,7 +38,9 @@ class StateBase(model_base.ObjectBase): if isinstance(prop, model_base.ObjectProperty): mutation_type = 'update_objproperty' else: - assert isinstance(prop, model_base.Property) + assert isinstance( + prop, (model_base.Property, + model_base.ObjectReferenceProperty)) mutation_type = 'update_property' root.handle_mutation( diff --git a/noisicaa/music/track.py b/noisicaa/music/track.py index 6bb9352d..9fa7a424 100644 --- a/noisicaa/music/track.py +++ b/noisicaa/music/track.py @@ -121,10 +121,6 @@ class Track(model.Track, state.StateBase): self.name = name self.instrument = instrument - self._instrument_added = False - self._event_source_added = False - self._mixer_added = False - @property def project(self): return self.sheet.project @@ -194,125 +190,61 @@ class Track(model.Track, state.StateBase): return '%s-events' % self.id def add_mixer_to_pipeline(self): - assert not self._mixer_added - - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.AddNode( 'passthru', self.mixer_name, 'track-mixer')) - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.ConnectPorts( self.mixer_name, 'out', self.sheet.main_mixer_name, 'in')) - if self._instrument_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.ConnectPorts( - self.instr_name, 'out', self.mixer_name, 'in')) - - self._mixer_added = True - def remove_mixer_from_pipeline(self): - if not self._mixer_added: - return - - if self._instrument_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.DisconnectPorts( - self.instr_name, 'out', self.mixer_name, 'in')) - - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.DisconnectPorts( self.mixer_name, 'out', self.sheet.main_mixer_name, 'in')) - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.RemoveNode(self.mixer_name)) - self._mixer_added = False - def add_instrument_to_pipeline(self): - assert not self._instrument_added - self.instrument.add_to_pipeline() - if self._mixer_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.ConnectPorts( - self.instr_name, 'out', self.mixer_name, 'in')) - - if self._event_source_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.ConnectPorts( - self.event_source_name, 'out', self.instr_name, 'in')) + self.sheet.handle_pipeline_mutation( + mutations.ConnectPorts( + self.instr_name, 'out', self.mixer_name, 'in')) - self._instrument_added = True + self.sheet.handle_pipeline_mutation( + mutations.ConnectPorts( + self.event_source_name, 'out', self.instr_name, 'in')) def remove_instrument_from_pipeline(self): - if not self._instrument_added: - return - - if self._mixer_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.DisconnectPorts( - self.instr_name, 'out', self.mixer_name, 'in')) + self.sheet.handle_pipeline_mutation( + mutations.DisconnectPorts( + self.instr_name, 'out', self.mixer_name, 'in')) - if self._event_source_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.DisconnectPorts( - self.event_source_name, 'out', self.instr_name, 'in')) + self.sheet.handle_pipeline_mutation( + mutations.DisconnectPorts( + self.event_source_name, 'out', self.instr_name, 'in')) self.instrument.remove_from_pipeline() - self._instrument_added = False - def add_event_source_to_pipeline(self): - assert not self._event_source_added - - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.AddNode( 'track_event_source', self.event_source_name, 'events')) - if self._instrument_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.ConnectPorts( - self.event_source_name, 'out', self.instr_name, 'in')) - - self._event_source_added = True - def remove_event_source_from_pipeline(self): - if not self._event_source_added: - return - - if self._instrument_added: - self.project.listeners.call( - 'pipeline_mutations', - mutations.DisconnectPorts( - self.event_source_name, 'out', self.instr_name, 'in')) - - self.project.listeners.call( - 'pipeline_mutations', + self.sheet.handle_pipeline_mutation( mutations.RemoveNode(self.event_source_name)) - self._event_source_added = False - def add_to_pipeline(self): self.add_mixer_to_pipeline() + self.add_event_source_to_pipeline() if self.instrument is not None: self.add_instrument_to_pipeline() - self.add_event_source_to_pipeline() def remove_from_pipeline(self): + if self.instrument is not None: + self.remove_instrument_from_pipeline() self.remove_event_source_from_pipeline() - self.remove_instrument_from_pipeline() self.remove_mixer_from_pipeline() diff --git a/noisicaa/ui/editor_app.py b/noisicaa/ui/editor_app.py index 84c2dec1..71353ae7 100644 --- a/noisicaa/ui/editor_app.py +++ b/noisicaa/ui/editor_app.py @@ -193,22 +193,10 @@ class BaseEditorApp(QApplication): if project.path)) async def addProject(self, project_connection): - self.win.addProjectView(project_connection) - project_connection.playback_node = await self.audioproc_client.add_node( - 'ipc', address=project_connection.client.audiostream_address) - await self.audioproc_client.connect_ports( - project_connection.playback_node, 'out', 'sink', 'in') - self._updateOpenedProjects() + await self.win.addProjectView(project_connection) async def removeProject(self, project_connection): - if project_connection.playback_node is not None: - await self.audioproc_client.disconnect_ports( - project_connection.playback_node, 'out', 'sink', 'in') - await self.audioproc_client.remove_node( - project_connection.playback_node) - project_connection.playback_node = None - - self.win.removeProjectView(project_connection) + await self.win.removeProjectView(project_connection) self._updateOpenedProjects() await self.project_registry.close_project(project_connection) diff --git a/noisicaa/ui/editor_window.py b/noisicaa/ui/editor_window.py index e824961a..6f485097 100644 --- a/noisicaa/ui/editor_window.py +++ b/noisicaa/ui/editor_window.py @@ -400,19 +400,22 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow): self.currentProjectChanged.emit(None) self.currentSheetChanged.emit(None) - def addProjectView(self, project): - view = ProjectView(**self.context, project_connection=project) + async def addProjectView(self, project_connection): + view = ProjectView( + **self.context, project_connection=project_connection) + await view.setup() + view.setCurrentTool(self.tools_dock.currentTool()) self.tools_dock.toolChanged.connect(view.setCurrentTool) - idx = self._project_tabs.addTab(view, project.name) + idx = self._project_tabs.addTab(view, project_connection.name) self._project_tabs.setCurrentIndex(idx) self._close_current_project_action.setEnabled(True) self._add_score_track_action.setEnabled(True) self._main_area.setCurrentIndex(0) - def removeProjectView(self, project_connection): + async def removeProjectView(self, project_connection): for idx in range(self._project_tabs.count()): view = self._project_tabs.widget(idx) if view.project_connection is project_connection: @@ -423,6 +426,8 @@ class EditorWindow(ui_base.CommonMixin, QMainWindow): self._project_tabs.count() > 0) self._add_score_track_action.setEnabled( self._project_tabs.count() > 0) + + await view.cleanup() break else: raise ValueError("No view for project found.") diff --git a/noisicaa/ui/project_registry.py b/noisicaa/ui/project_registry.py index 2e50cc4b..ab623ead 100644 --- a/noisicaa/ui/project_registry.py +++ b/noisicaa/ui/project_registry.py @@ -18,8 +18,6 @@ class Project(object): self.process_address = None self.client = None - self.playback_node = None - @property def name(self): return os.path.basename(self.path) diff --git a/noisicaa/ui/project_view.py b/noisicaa/ui/project_view.py index e3ba976a..fee5495a 100644 --- a/noisicaa/ui/project_view.py +++ b/noisicaa/ui/project_view.py @@ -22,19 +22,11 @@ class ProjectViewImpl(QtWidgets.QWidget): super().__init__(**kwargs) self._sheets_widget = QtWidgets.QStackedWidget(self) - for sheet in self.project.sheets: - view = self.createSheetView( - **self.context, sheet=sheet, parent=self) - self._sheets_widget.addWidget(view) self._sheets_widget.currentChanged.connect( self.onCurrentSheetChanged) self._current_sheet_view = None - # TODO: Persist what was the current sheet. - if self._sheets_widget.count() > 0: - self.setCurrentSheetView(self._sheets_widget.widget(0)) - self.sheet_menu = QtWidgets.QMenu() self.updateSheetMenu() @@ -57,7 +49,26 @@ class ProjectViewImpl(QtWidgets.QWidget): self.setLayout(layout) self._sheet_listener = self.project.listeners.add( - 'sheets', self.onSheetsChanged) + 'sheets', + lambda *args, **kwargs: self.call_async( + self.onSheetsChanged(*args, **kwargs))) + + async def setup(self): + for sheet in self.project.sheets: + view = self.createSheetView( + **self.context, sheet=sheet, parent=self) + await view.setup() + self._sheets_widget.addWidget(view) + + # TODO: Persist what was the current sheet. + if self._sheets_widget.count() > 0: + self.setCurrentSheetView(self._sheets_widget.widget(0)) + + async def cleanup(self): + while self._sheets_widget.count() > 0: + sheet_view = self._sheets_widget.widget(0) + self._sheets_widget.removeWidget(sheet_view) + await sheet_view.cleanup() @property def sheetViews(self): @@ -99,35 +110,27 @@ class ProjectViewImpl(QtWidgets.QWidget): for sheet_view in self.sheetViews: sheet_view.updateView() - def closeEvent(self, event): - logger.info("CloseEvent received: %s", event) - - self._sheet_listener.remove() - - while self._sheets_widget.count() > 0: - sheet_view = self._sheets_widget.widget(0) - self._sheets_widget.removeWidget(sheet_view) - sheet_view.close() - - event.accept() - - def onSheetsChanged(self, action, *args): + async def onSheetsChanged(self, action, *args): if action == 'insert': idx, sheet = args view = self.createSheetView( **self.context, sheet=sheet, parent=self) - #view.setCurrentTool(self.currentTool()) + await view.setup() self._sheets_widget.insertWidget(idx, view) self.updateSheetMenu() elif action == 'delete': idx, = args - self._sheets_widget.removeWidget(self._sheets_widget.widget(idx)) + view = self._sheets_widget.widget(idx) + self._sheets_widget.removeWidget(view) + await view.cleanup() self.updateSheetMenu() elif action == 'clear': while self._sheets_widget.count() > 0: - self._sheets_widget.removeWidget(self._sheets_widget.widget(0)) + view = self._sheets_widget.widget(0) + self._sheets_widget.removeWidget(view) + await view.cleanup() self.updateSheetMenu() else: # pragma: no cover @@ -160,8 +163,7 @@ class ProjectViewImpl(QtWidgets.QWidget): self.setCurrentSheetView(sheet_view) def onAddSheet(self): - self.send_command_async( - self.project.id, 'AddSheet') + self.send_command_async(self.project.id, 'AddSheet') def onDeleteSheet(self): assert len(self.project.sheets) > 1 diff --git a/noisicaa/ui/project_view_test.py b/noisicaa/ui/project_view_test.py index 1e1daa48..b8047219 100644 --- a/noisicaa/ui/project_view_test.py +++ b/noisicaa/ui/project_view_test.py @@ -25,6 +25,12 @@ class SheetView(uitest_utils.TestMixin, sheet_view.SheetViewImpl): 'SheetPropertyTrack': SheetPropertyTrackItem, } + async def setup(self): + pass + + async def cleanup(self): + pass + class ProjectView(uitest_utils.TestMixin, project_view.ProjectViewImpl): def createSheetView(self, **kwargs): return SheetView(**kwargs) @@ -33,6 +39,7 @@ class ProjectView(uitest_utils.TestMixin, project_view.ProjectViewImpl): class InitTest(uitest_utils.UITest): async def test_init_with_no_sheets(self): view = ProjectView(**self.context) + await view.setup() self.assertIsNone(view.currentSheetView()) async def test_init_with_sheets(self): @@ -41,6 +48,7 @@ class InitTest(uitest_utils.UITest): self.project.sheets.append(model.Sheet('sheet2')) self.project.sheets[1].property_track = model.SheetPropertyTrack('prop2') view = ProjectView(**self.context) + await view.setup() self.assertIsInstance(view.currentSheetView(), SheetView) self.assertEqual(view.currentSheetView().sheet.id, 'sheet1') for sheet_view in view.sheetViews: @@ -49,49 +57,43 @@ class InitTest(uitest_utils.UITest): class ModelChangesTest(uitest_utils.UITest): - async def test_add_sheet(self): - view = ProjectView(**self.context) - self.assertEqual(len(list(view.sheetViews)), 0) - - sheet = model.Sheet('sheet1') - sheet.property_track = model.SheetPropertyTrack('prop1') - self.project.sheets.append(sheet) - self.assertEqual(len(list(view.sheetViews)), 1) - - async def test_remove_sheet(self): - sheet = model.Sheet('sheet1') - sheet.property_track = model.SheetPropertyTrack('prop1') - self.project.sheets.append(sheet) + # TODO: the change handler is async, need to wait until it was run + # before checking that state of the sheetViews list. + pass - view = ProjectView(**self.context) - self.assertEqual(len(list(view.sheetViews)), 1) + # async def test_add_sheet(self): + # view = ProjectView(**self.context) + # await view.setup() + # self.assertEqual(len(list(view.sheetViews)), 0) - del self.project.sheets[0] - self.assertEqual(len(list(view.sheetViews)), 0) + # sheet = model.Sheet('sheet1') + # sheet.property_track = model.SheetPropertyTrack('prop1') + # self.project.sheets.append(sheet) + # self.assertEqual(len(list(view.sheetViews)), 1) - async def test_clear_sheets(self): - sheet = model.Sheet('sheet1') - sheet.property_track = model.SheetPropertyTrack('prop1') - self.project.sheets.append(sheet) + # async def test_remove_sheet(self): + # sheet = model.Sheet('sheet1') + # sheet.property_track = model.SheetPropertyTrack('prop1') + # self.project.sheets.append(sheet) - view = ProjectView(**self.context) - self.assertEqual(len(list(view.sheetViews)), 1) + # view = ProjectView(**self.context) + # await view.setup() + # self.assertEqual(len(list(view.sheetViews)), 1) - self.project.sheets.clear() - self.assertEqual(len(list(view.sheetViews)), 0) + # del self.project.sheets[0] + # self.assertEqual(len(list(view.sheetViews)), 0) + # async def test_clear_sheets(self): + # sheet = model.Sheet('sheet1') + # sheet.property_track = model.SheetPropertyTrack('prop1') + # self.project.sheets.append(sheet) -class EventsTest(uitest_utils.UITest): - async def test_closeEvent(self): - sheet = model.Sheet('sheet1') - sheet.property_track = model.SheetPropertyTrack('prop1') - self.project.sheets.append(sheet) - - view = ProjectView(**self.context) + # view = ProjectView(**self.context) + # await view.setup() + # self.assertEqual(len(list(view.sheetViews)), 1) - event = QtGui.QCloseEvent() - view.closeEvent(event) - self.assertEqual(len(list(view.sheetViews)), 0) + # self.project.sheets.clear() + # self.assertEqual(len(list(view.sheetViews)), 0) class CommandsTest(uitest_utils.UITest): @@ -102,6 +104,7 @@ class CommandsTest(uitest_utils.UITest): self.project.sheets.append(sheet) view = ProjectView(**self.context) + await view.setup() view.onAddSheet() self.assertEqual( self.commands, @@ -118,6 +121,7 @@ class CommandsTest(uitest_utils.UITest): self.project.sheets.append(sheet) view = ProjectView(**self.context) + await view.setup() view.onDeleteSheet() self.assertEqual( self.commands, diff --git a/noisicaa/ui/sheet_view.py b/noisicaa/ui/sheet_view.py index 3ee6ca51..50e3af01 100644 --- a/noisicaa/ui/sheet_view.py +++ b/noisicaa/ui/sheet_view.py @@ -1238,7 +1238,7 @@ class SheetViewImpl(QGraphicsView): self._layers[layer_id] = layer self._scene.addItem(layer) - self._current_tool = -1 + self._current_tool = Tool.NOTE_QUARTER self._previous_tool = -1 self._cursor = QGraphicsGroup(self._layers[Layer.MOUSE]) @@ -1247,6 +1247,33 @@ class SheetViewImpl(QGraphicsView): self._tracks_listener = self._sheet.listeners.add( 'tracks', self.onTracksChanged) + self._player_id = None + self._player_stream_address = None + self._player_node_id = None + + async def setup(self): + self._player_id, self._player_stream_address = await self.project_client.create_player(self._sheet.id) + self._player_node_id = await self.audioproc_client.add_node( + 'ipc', address=self._player_stream_address) + await self.audioproc_client.connect_ports( + self._player_node_id, 'out', 'sink', 'in') + + async def cleanup(self): + while len(self._tracks) > 0: + self.removeTrack(0) + + if self._player_node_id is not None: + await self.audioproc_client.disconnect_ports( + self._player_node_id, 'out', 'sink', 'in') + await self.audioproc_client.remove_node( + self._player_node_id) + self._player_node_id = None + self._player_stream_address = None + + if self._player_id is not None: + await self.project_client.delete_player(self._player_id) + self._player_id = None + @property def trackItems(self): return list(self._tracks) @@ -1275,14 +1302,6 @@ class SheetViewImpl(QGraphicsView): del self._tracks[idx] del self._track_visible_listeners[idx] - def closeEvent(self, event): - self._tracks_listener.remove() - - while len(self._tracks) > 0: - self.removeTrack(0) - - event.accept() - def onVisibleChanged(self, old_value, new_value): self.setVisible(new_value) diff --git a/noisicaa/ui/sheet_view_test.py b/noisicaa/ui/sheet_view_test.py index 1f435be4..846ef1dd 100644 --- a/noisicaa/ui/sheet_view_test.py +++ b/noisicaa/ui/sheet_view_test.py @@ -37,6 +37,12 @@ class SheetView(uitest_utils.TestMixin, sheet_view.SheetViewImpl): 'ScoreTrack': ScoreTrackItem, } + async def setup(self): + pass + + async def cleanup(self): + pass + class SheetViewTest(uitest_utils.UITest): async def setUp(self): @@ -53,6 +59,7 @@ class SheetViewInitTest(SheetViewTest): track = model.ScoreTrack('track1') self.sheet.tracks.append(track) view = SheetView(**self.context, sheet=self.sheet) + await view.setup() self.assertEqual( [ti.track.id for ti in view.trackItems], ['track1']) @@ -61,6 +68,7 @@ class SheetViewInitTest(SheetViewTest): class SheetViewModelChangesTest(SheetViewTest): async def test_tracks(self): view = SheetView(**self.context, sheet=self.sheet) + await view.setup() track = model.ScoreTrack('track1') self.sheet.tracks.append(track) @@ -85,6 +93,7 @@ class SheetViewModelChangesTest(SheetViewTest): track = model.ScoreTrack('track1') self.sheet.tracks.append(track) view = SheetView(**self.context, sheet=self.sheet) + await view.setup() view.updateSheet = mock.MagicMock() self.assertEqual(view.updateSheet.call_count, 0) track.visible = False @@ -94,6 +103,7 @@ class SheetViewModelChangesTest(SheetViewTest): class SheetViewCommandsTest(SheetViewTest): async def test_onAddTrack(self): view = SheetView(**self.context, sheet=self.sheet) + await view.setup() view.onAddTrack('score') self.assertEqual( self.commands, @@ -101,15 +111,9 @@ class SheetViewCommandsTest(SheetViewTest): class SheetViewEventsTest(SheetViewTest): - async def test_closeEvent(self): - track = model.ScoreTrack('track1') - self.sheet.tracks.append(track) - view = SheetView(**self.context, sheet=self.sheet) - view.closeEvent(QtGui.QCloseEvent()) - self.assertEqual(len(view.trackItems), 0) - async def test_keyEvent(self): view = SheetView(**self.context, sheet=self.sheet) + await view.setup() # Number keys select notes, if the current tool is a note. view.setCurrentTool(tool_dock.Tool.NOTE_QUARTER) @@ -188,7 +192,7 @@ class SheetViewToolTest(SheetViewTest): # This one just aims for coverage. TODO: also verify effects. async def test_setCurrentTool(self): view = SheetView(**self.context, sheet=self.sheet) - self.assertEqual(view.currentTool(), -1) + await view.setup() for tool in tool_dock.Tool: with self.subTest(tool=tool): diff --git a/noisicaa/ui/uitest_utils.py b/noisicaa/ui/uitest_utils.py index 794fc82f..d38d440a 100644 --- a/noisicaa/ui/uitest_utils.py +++ b/noisicaa/ui/uitest_utils.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import argparse +import functools import unittest import inspect import logging @@ -54,6 +55,17 @@ class TestMixin(object): def project_client(self): return self.__testcase.project_client + def call_async(self, coroutine, callback=None): + task = self.event_loop.create_task(coroutine) + task.add_done_callback( + functools.partial(self.__call_async_cb, callback=callback)) + + def __call_async_cb(self, task, callback): + if task.exception() is not None: + raise task.exception() + if callback is not None: + callback(task.result()) + def send_command_async(self, target_id, cmd, **kwargs): self.__testcase.commands.append((target_id, cmd, kwargs))