Completely rewrote sheet rendering:

- Don't use QGraphicsView anymore. It was convenient, but got very slow for large sheets.
- Use widgets with custom paint method, just painting what's on the screen.
- Split sheet view into three separate widgets:
  - SheetEditor with the sheet itself
  - Static TimeLine at the top.
  - Static TrackList at the left (not yet used for much).
- Rendering of Score measures is somewhat degraded...
- Batching of updates has been removed, but doesn't seem to be needed right now (Qt's batching of widget updates seems to do the trick well enough).
- Created separate package for SheetView related classes.
- I'm a slacker re unittests :(
looper
Ben Niemann 2016-11-12 18:05:34 +01:00
parent 73d97421a1
commit ab0887f438
61 changed files with 5285 additions and 4263 deletions

View File

@ -1,18 +1,50 @@
# -*- org-tags-column: -98 -*-
* SheetEditor: show/hide tracks does work anymore :BUG:
* ScoreEditorTrackItem: Improve rendering :FR:
** ghost notes should be closer to real insert position
** squeeze notes into measure, if duration is exceeded
** render exceeding notes differently
** proper chord rendering
** note beams
* Exception when reordering tracks :CRASH:
Traceback (most recent call last):
File "/storage/users/pink/projects/noisicaä/noisicaa/ui/tracks_dock.py", line 499, in onCurrentChanged
not track.is_master_group and not track.is_first)
File "/storage/users/pink/projects/noisicaä/noisicaa/core/model_base.py", line 410, in is_first
raise NotListMemberError(self.id)
noisicaa.core.model_base.NotListMemberError: 32e1b62e20524d16a584c65311960356
* Exception on shutdown :CRASH:
Traceback (most recent call last):
File "/storage/users/pink/projects/noisicaä/noisicaa/core/process_manager.py", line 236, in start_process
rc = impl.main(ready_callback)
File "/storage/users/pink/projects/noisicaä/noisicaa/core/process_manager.py", line 386, in main
self.main_async(ready_callback, *args, **kwargs))
File "/usr/lib/python3.5/asyncio/base_events.py", line 387, in run_until_complete
return future.result()
File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result
raise self._exception
File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
result = coro.send(None)
File "/storage/users/pink/projects/noisicaä/noisicaa/core/process_manager.py", line 409, in main_async
await self.cleanup()
File "/storage/users/pink/projects/noisicaä/noisicaa/music/project_process.py", line 225, in cleanup
await self.node_db.cleanup()
File "/storage/users/pink/projects/noisicaä/noisicaa/node_db/client.py", line 34, in cleanup
await self.disconnect()
File "/storage/users/pink/projects/noisicaä/noisicaa/node_db/client.py", line 46, in disconnect
await self._stub.call('END_SESSION', self._session_id)
File "/storage/users/pink/projects/noisicaä/noisicaa/core/ipc.py", line 357, in call
raise ConnectionClosed
noisicaa.core.ipc.ConnectionClosed
* when changing scale_x, keep view centered on current position :FR:
* control playback position :ANNOYING:
- rewind to start
- move manually
- loop range
* TODO redesign sheet rendering :CLEANUP:
- is QGraphicsView appropriate?
- becomes super slow with long samples
- sample rendering is cut off at some point
- or rather a generic QWidget with custom rendering
- e.g. long samples at high zoom level leads to extremely big QGraphicsItem
- also makes stuff like fixed track controls on the left hand side or time meter on the top easier.
* clarify time handling :CLEANUP:
- musical time
- base unit full note (4 beats)
@ -120,10 +152,6 @@ Library UI
- 'scanning', #files_done, #files_total
- 'done', done_timestamp, #inst_added, #inst_remove, #inst_updated
* transposing a measure works only once :BUG:
need to move the mouse again to before it works again
MeasureItem loses focus?
* Session state :FR:
- store binary log for efficiency
- replay log on open
@ -262,6 +290,7 @@ When reparenting a track, also reparent its mixer node.
- or separate setup() method?
- all object references (child, lists, etc.) only store IDs in state, do
lazy dereferencing on __get__
* Use "def foo(*, ...)" to enforce keyword-only functions :CLEANUP:
* BeatTrack: move beats to arbitrary positions :FR:
* Sometimes hangs during shutdown :BUG:
Last sign of life:

119
data/icons/pointer.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30"
id="svg2"
version="1.1"
viewBox="147 151 60 60"
width="30"
sodipodi:docname="track-type-beat.svg"
inkscape:version="0.91 r13725">
<metadata
id="metadata3495">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1371"
id="namedview3493"
showgrid="false"
inkscape:zoom="11.313709"
inkscape:cx="3.8886379"
inkscape:cy="19.745318"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<defs
id="defs10" />
<g
id="layer2"
style="display:inline"
transform="translate(0,-637.6694)"
inkscape:groupmode="layer"
inkscape:label="Symbol">
<path
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.55646701pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 147.5491,818.6694 58.9018,0"
id="path4608"
inkscape:connector-curvature="0" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.5pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 150,807.74998 0,21.75045 4.8125,-10.85216 z"
id="path4373"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.5pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 178.03125,807.79417 0,21.75045 4.8125,-10.85216 z"
id="path4373-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.5pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 191.28125,807.79417 0,21.75045 4.8125,-10.85216 z"
id="path4373-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30"
id="svg2"
version="1.1"
viewBox="147 151 60 60"
width="30"
sodipodi:docname="track-type-control.svg"
inkscape:version="0.91 r13725">
<metadata
id="metadata3495">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1371"
id="namedview3493"
showgrid="false"
inkscape:zoom="32"
inkscape:cx="4.2798818"
inkscape:cy="15.099995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<defs
id="defs10" />
<g
id="layer2"
style="display:inline"
transform="translate(0,-637.6694)"
inkscape:groupmode="layer"
inkscape:label="Symbol">
<path
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.55646701pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 147.5491,790.6694 58.9018,0"
id="path4608"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.55646701pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 147.5491,846.6694 58.9018,0"
id="path4608-7"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3.224;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 148.34375,835.6069 21.6875,0 16.1875,-31.125 19.4375,0"
id="path4477"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -24,6 +24,7 @@ class AudioProcClientMixin(object):
log_level=-1)
async def cleanup(self):
await self.disconnect()
self.server.remove_command_handler('PIPELINE_MUTATION')
self.server.remove_command_handler('PIPELINE_STATUS')
await super().cleanup()

View File

@ -30,8 +30,10 @@ class Session(object):
self.id = uuid.uuid4().hex
self.pending_mutations = []
def cleanup(self):
pass
async def cleanup(self):
if self.callback_stub is not None:
await self.callback_stub.close()
self.callback_stub = None
def publish_mutation(self, mutation):
if not self.callback_stub.connected:
@ -150,6 +152,10 @@ class AudioProcProcessMixin(object):
self.sessions = {}
async def cleanup(self):
for session in self.sessions.values():
session.cleanup()
self.sessions.clear()
if self.shm is not None:
self.shm.close_fd()
self.shm = None
@ -219,9 +225,9 @@ class AudioProcProcessMixin(object):
session.callback_stub_connected()
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]
async def handle_shutdown(self):

View File

@ -4,6 +4,7 @@ import unittest
import asynctest
from noisicaa import node_db
from .exceptions import Error
from .node import Node
from .ports import InputPort, OutputPort
@ -14,15 +15,15 @@ class PipelineTest(asynctest.TestCase):
async def testSortedNodes(self):
p = pipeline.Pipeline()
n1 = Node(self.loop)
n1 = Node(self.loop, node_db.NodeDescription())
p.add_node(n1)
n1.add_output(OutputPort('p'))
n2 = Node(self.loop)
n2 = Node(self.loop, node_db.NodeDescription())
p.add_node(n2)
n2.add_output(OutputPort('p'))
n3 = Node(self.loop)
n3 = Node(self.loop, node_db.NodeDescription())
p.add_node(n3)
n3.add_input(InputPort('p1'))
n3.add_input(InputPort('p2'))
@ -30,11 +31,11 @@ class PipelineTest(asynctest.TestCase):
n3.inputs['p2'].connect(n2.outputs['p'])
n3.add_output(OutputPort('p'))
n4 = Node(self.loop)
n4 = Node(self.loop, node_db.NodeDescription())
p.add_node(n4)
n4.add_output(OutputPort('p'))
n5 = Node(self.loop)
n5 = Node(self.loop, node_db.NodeDescription())
p.add_node(n5)
n5.add_input(InputPort('p1'))
n5.add_input(InputPort('p2'))
@ -50,23 +51,23 @@ class PipelineTest(asynctest.TestCase):
async def testCyclicGraph(self):
p = pipeline.Pipeline()
n1 = Node(self.loop)
n1 = Node(self.loop, node_db.NodeDescription())
p.add_node(n1)
n1.add_output(OutputPort('p'))
n2 = Node(self.loop)
n2 = Node(self.loop, node_db.NodeDescription())
p.add_node(n2)
n2.add_input(InputPort('p1'))
n2.add_input(InputPort('p2'))
n2.add_output(OutputPort('p'))
n3 = Node(self.loop)
n3 = Node(self.loop, node_db.NodeDescription())
p.add_node(n3)
n3.add_input(InputPort('p'))
n3.add_output(OutputPort('p1'))
n3.add_output(OutputPort('p2'))
n4 = Node(self.loop)
n4 = Node(self.loop, node_db.NodeDescription())
p.add_node(n4)
n4.add_input(InputPort('p'))
n4.add_output(OutputPort('p'))

View File

@ -267,6 +267,7 @@ class Stub(object):
self._transport = None
self._protocol = None
self._command_queue = None
self._command_loop_cancelled = None
self._command_loop_task = None
@property
@ -282,11 +283,13 @@ class Stub(object):
logger.info("Connected to server at %s", self._server_address)
self._command_queue = asyncio.Queue(loop=self._event_loop)
self._command_loop_cancelled = asyncio.Event(loop=self._event_loop)
self._command_loop_task = self._event_loop.create_task(self.command_loop())
async def close(self):
if self._command_loop_task is not None:
self._command_loop_task.cancel()
self._command_loop_cancelled.set()
await asyncio.wait_for(self._command_loop_task, None)
self._command_loop_task = None
if self._command_queue is not None:
@ -308,8 +311,18 @@ class Stub(object):
return False
async def command_loop(self):
while True:
cmd, payload, response_container = await self._command_queue.get()
cancelled_task = asyncio.ensure_future(self._command_loop_cancelled.wait())
while not self._command_loop_cancelled.is_set():
get_task = asyncio.ensure_future(self._command_queue.get())
done, pending = await asyncio.wait(
[get_task, cancelled_task],
return_when=asyncio.FIRST_COMPLETED)
if get_task not in done:
get_task.cancel()
asyncio.gather(get_task, return_exceptions=True)
continue
cmd, payload, response_container = get_task.result()
if self._transport.is_closing():
response_container.set(self.CLOSE_SENTINEL)
@ -322,6 +335,9 @@ class Stub(object):
response = await self._protocol.response_queue.get()
response_container.set(response)
cancelled_task.cancel()
asyncio.gather(cancelled_task, return_exceptions=True)
async def call(self, cmd, *args, **kwargs):
if not isinstance(cmd, bytes):
cmd = cmd.encode('ascii')

View File

@ -8,3 +8,4 @@ from .mutations import (
AddInstrumentDescription,
RemoveInstrumentDescription,
)
from .process_base import InstrumentDBProcessBase

View File

@ -11,6 +11,7 @@ from noisicaa import core
from noisicaa.core import ipc
from .private import db
from . import process_base
logger = logging.getLogger(__name__)
@ -26,8 +27,10 @@ class Session(object):
self.id = uuid.uuid4().hex
self.pending_mutations = []
def cleanup(self):
pass
async def cleanup(self):
if self.callback_stub is not None:
await self.callback_stub.close()
self.callback_stub = None
def publish_mutations(self, mutations):
if not mutations:
@ -57,7 +60,7 @@ class Session(object):
self.pending_mutations.clear()
class InstrumentDBProcessMixin(object):
class InstrumentDBProcessMixin(process_base.InstrumentDBProcessBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sessions = {}
@ -73,14 +76,6 @@ class InstrumentDBProcessMixin(object):
self._shutting_down = asyncio.Event()
self._shutdown_complete = asyncio.Event()
self.server.add_command_handler(
'START_SESSION', self.handle_start_session)
self.server.add_command_handler(
'END_SESSION', self.handle_end_session)
self.server.add_command_handler('SHUTDOWN', self.handle_shutdown)
self.server.add_command_handler(
'START_SCAN', self.handle_start_scan)
self.db = db.InstrumentDB(self.event_loop, constants.CACHE_DIR)
self.db.setup()
self.db.add_mutations_listener(self.publish_mutations)
@ -88,6 +83,10 @@ class InstrumentDBProcessMixin(object):
self.db.start_scan(self.search_paths, True)
async def cleanup(self):
for session in self.sessions.values():
await session.cleanup()
self.sessions.clear()
if self.db is not None:
self.db.cleanup()
self.db = None
@ -132,9 +131,9 @@ class InstrumentDBProcessMixin(object):
session.callback_stub_connected()
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]
async def handle_shutdown(self):

View File

@ -0,0 +1,26 @@
#!/usr/bin/python3
class InstrumentDBProcessBase(object):
async def setup(self):
await super().setup()
self.server.add_command_handler(
'START_SESSION', self.handle_start_session)
self.server.add_command_handler(
'END_SESSION', self.handle_end_session)
self.server.add_command_handler('SHUTDOWN', self.handle_shutdown)
self.server.add_command_handler(
'START_SCAN', self.handle_start_scan)
def handle_start_session(self, client_address, flags):
raise NotImplementedError
def handle_end_session(self, session_id):
raise NotImplementedError
async def handle_shutdown(self):
raise NotImplementedError
async def handle_start_scan(self, session_id):
raise NotImplementedError

View File

@ -80,6 +80,7 @@ class AddBeat(commands.Command):
assert isinstance(measure, BeatMeasure)
beat = Beat(timepos=self.timepos, velocity=100)
assert 0 <= beat.timepos < measure.duration
measure.beats.append(beat)
commands.Command.register_command(AddBeat)

View File

@ -23,11 +23,13 @@ from . import player
logger = logging.getLogger(__name__)
class MockAudioProcClient(object):
def __init__(self, event_loop, server):
self.audiostream_server = None
self.backend_thread = None
self.stop_backend = None
self.listeners = core.CallbackRegistry()
async def setup(self):
pass
@ -83,7 +85,7 @@ class MockAudioProcClient(object):
class PlayerTest(asynctest.TestCase):
async def setUp(self):
self.project = project.BaseProject()
self.sheet = sheet.Sheet(name='Test', num_tracks=0)
self.sheet = sheet.Sheet(name='Test')
self.project.sheets.append(self.sheet)
self.player_status_calls = asyncio.Queue()
@ -97,7 +99,7 @@ class PlayerTest(asynctest.TestCase):
await self.audioproc_server.setup()
self.mock_manager = mock.Mock()
async def mock_call(cmd, *args):
async def mock_call(cmd, *args, **kwargs):
assert cmd == 'CREATE_AUDIOPROC_PROCESS'
name, = args
assert name == 'player'

View File

@ -222,7 +222,7 @@ class BaseProject(model.Project, state.RootMixin, state.StateBase):
@classmethod
def make_demo(cls):
project = cls()
s = sheet.Sheet(name="Demo Sheet", num_tracks=0)
s = sheet.Sheet(name="Demo Sheet")
project.add_sheet(s)
while len(s.property_track.measure_list) < 5:

View File

@ -84,6 +84,10 @@ class ProjectClientMixin(object):
self.server.add_command_handler(
'SESSION_DATA_MUTATION', self.handle_session_data_mutation)
async def cleanup(self):
await self.disconnect()
await super().cleanup()
async def connect(self, address):
assert self._stub is None
self._stub = ipc.Stub(self.event_loop, address)

View File

@ -9,6 +9,7 @@ from unittest import mock
import asynctest
from noisicaa import core
from noisicaa import node_db
from noisicaa.core import ipc
from noisicaa.ui import model
@ -35,9 +36,10 @@ class TestClient(project_client.ProjectClientMixin, TestClientImpl):
class TestProjectProcessImpl(object):
def __init__(self, event_loop):
def __init__(self, event_loop, manager):
super().__init__()
self.event_loop = event_loop
self.manager = manager
self.server = ipc.Server(self.event_loop, 'project')
async def setup(self):
@ -51,9 +53,47 @@ class TestProjectProcess(
project_process.ProjectProcessMixin, TestProjectProcessImpl):
pass
class AsyncSetupBase():
async def setup(self):
pass
async def cleanup(self):
pass
class TestNodeDBProcess(node_db.NodeDBProcessBase, AsyncSetupBase):
def __init__(self, event_loop):
super().__init__()
self.event_loop = event_loop
self.server = ipc.Server(self.event_loop, 'node_db')
async def setup(self):
await super().setup()
await self.server.setup()
async def cleanup(self):
await self.server.cleanup()
await super().cleanup()
def handle_start_session(self, client_address, flags):
return '123'
def handle_end_session(self, session_id):
return None
class ProxyTest(asynctest.TestCase):
async def setUp(self):
self.project_process = TestProjectProcess(self.loop)
self.node_db_process = TestNodeDBProcess(self.loop)
await self.node_db_process.setup()
self.manager = mock.Mock()
async def mock_call(cmd, *args, **kwargs):
self.assertEqual(cmd, 'CREATE_NODE_DB_PROCESS')
return self.node_db_process.server.address
self.manager.call.side_effect = mock_call
self.project_process = TestProjectProcess(self.loop, self.manager)
await self.project_process.setup()
self.client = TestClient(self.loop)
self.client.cls_map = model.cls_map
@ -65,6 +105,7 @@ class ProxyTest(asynctest.TestCase):
await self.client.disconnect()
await self.client.cleanup()
await self.project_process.cleanup()
await self.node_db_process.cleanup()
async def test_basic(self):
await self.client.create_inmemory()

View File

@ -47,6 +47,10 @@ class Session(object):
await p.cleanup()
self._players.clear()
if self.callback_stub is not None:
await self.callback_stub.close()
self.callback_stub = None
def get_player(self, player_id):
return self._players[player_id][1]
@ -84,14 +88,16 @@ class Session(object):
async def init_session_data(self, data_dir):
self.session_data = {}
self.session_data_path = os.path.join(data_dir, 'sessions', self.session_name)
if not os.path.isdir(self.session_data_path):
os.makedirs(self.session_data_path)
checkpoint_path = os.path.join(self.session_data_path, 'checkpoint')
if os.path.isfile(checkpoint_path):
with open(checkpoint_path, 'rb') as fp:
self.session_data = pickle.load(fp)
if data_dir is not None:
self.session_data_path = os.path.join(data_dir, 'sessions', self.session_name)
if not os.path.isdir(self.session_data_path):
os.makedirs(self.session_data_path)
checkpoint_path = os.path.join(self.session_data_path, 'checkpoint')
if os.path.isfile(checkpoint_path):
with open(checkpoint_path, 'rb') as fp:
self.session_data = pickle.load(fp)
await self.callback_stub.call('SESSION_DATA_MUTATION', self.session_data)
@ -112,8 +118,9 @@ class Session(object):
self.session_data.update(changes)
with open(os.path.join(self.session_data_path, 'checkpoint'), 'wb') as fp:
pickle.dump(self.session_data, fp)
if self.session_data_path is not None:
with open(os.path.join(self.session_data_path, 'checkpoint'), 'wb') as fp:
pickle.dump(self.session_data, fp)
if not from_client:
self.event_loop.create_task(
@ -210,9 +217,12 @@ class ProjectProcessMixin(object):
await self.node_db.connect(node_db_address)
async def cleanup(self):
if self.node_db is None:
self.node_db.close()
self.node_db.cleanup()
for session in self.sessions.values():
await session.cleanup()
self.sessions.clear()
if self.node_db is not None:
await self.node_db.cleanup()
self.node_db = None
await super().cleanup()
@ -308,7 +318,7 @@ class ProjectProcessMixin(object):
def _create_blank_project(self, project_cls):
project = project_cls(node_db=self.node_db)
s = sheet.Sheet(name='Sheet 1', num_tracks=0)
s = sheet.Sheet(name='Sheet 1')
project.add_sheet(s)
s.add_track(s.master_group, 0, score_track.ScoreTrack(name="Track 1"))
return project
@ -331,7 +341,7 @@ class ProjectProcessMixin(object):
await self.send_initial_mutations()
self.project.listeners.add(
'model_changes', self.handle_model_change)
await session.init_session_data(self.project.data_dir)
await session.init_session_data(None)
return self.project.id
async def handle_open(self, session_id, path):

View File

@ -108,8 +108,8 @@ class RenameSheetTest(unittest.TestCase):
class SetCurrentSheetTest(unittest.TestCase):
def test_ok(self):
p = project.BaseProject()
p.sheets.append(sheet.Sheet(name='Sheet 1'))
p.sheets.append(sheet.Sheet(name='Sheet 2'))
p.add_sheet(sheet.Sheet(name='Sheet 1'))
p.add_sheet(sheet.Sheet(name='Sheet 2'))
cmd = project.SetCurrentSheet(name='Sheet 2')
p.dispatch_command(p.id, cmd)
self.assertEqual(p.current_sheet, 1)
@ -175,21 +175,21 @@ class ProjectTest(unittest.TestCase):
self.fake_os.path.isfile('/foo.data/checkpoint.000001'))
class ScoreEventSource(unittest.TestCase):
def test_get_events(self):
proj = project.BaseProject()
s = sheet.Sheet(name='test')
proj.sheets.append(s)
track = project.ScoreTrack(name='test', num_measures=2)
s.master_group.tracks.append(track)
s.equalize_tracks()
# class ScoreEventSource(unittest.TestCase):
# def test_get_events(self):
# proj = project.BaseProject()
# s = sheet.Sheet(name='test')
# proj.sheets.append(s)
# track = project.ScoreTrack(name='test', num_measures=2)
# s.master_group.tracks.append(track)
# s.equalize_tracks()
source = track.create_event_source()
events = []
for sample_pos in range(0, 100000, 4096):
events.extend(source.get_events(sample_pos, sample_pos + 4096))
# source = track.create_event_source()
# events = []
# for sample_pos in range(0, 100000, 4096):
# events.extend(source.get_events(sample_pos, sample_pos + 4096))
print(events)
# print(events)
if __name__ == '__main__':

View File

@ -291,7 +291,7 @@ commands.Command.register_command(RemovePipelineGraphConnection)
class Sheet(model.Sheet, state.StateBase):
def __init__(self, name=None, num_tracks=1, state=None):
def __init__(self, name=None, state=None):
super().__init__(state)
if state is None:
@ -300,12 +300,6 @@ class Sheet(model.Sheet, state.StateBase):
self.master_group = track_group.MasterTrackGroup(name="Master")
self.property_track = sheet_property_track.SheetPropertyTrack(name="Time")
for i in range(num_tracks):
track = score_track.ScoreTrack(name="Track %d" % i)
self.add_track(
self.master_group, len(self.master_group.tracks),
track)
@property
def project(self):
return self.parent

View File

@ -11,8 +11,8 @@ from . import track_group
class SheetCommandTest(unittest.TestCase):
def setUp(self):
self.project = project.BaseProject()
self.sheet = sheet.Sheet(name='Test', num_tracks=0)
self.project.sheets.append(self.sheet)
self.sheet = sheet.Sheet(name='Test')
self.project.add_sheet(self.sheet)
class AddTrackTest(SheetCommandTest):
@ -44,7 +44,8 @@ class AddTrackTest(SheetCommandTest):
class DeleteTrackTest(SheetCommandTest):
def test_ok(self):
self.sheet.master_group.tracks.append(
self.sheet.add_track(
self.sheet.master_group, 0,
score_track.ScoreTrack(name='Test'))
cmd = sheet.RemoveTrack(
@ -53,7 +54,8 @@ class DeleteTrackTest(SheetCommandTest):
self.assertEqual(len(self.sheet.master_group.tracks), 0)
def test_track_with_instrument(self):
self.sheet.master_group.tracks.append(
self.sheet.add_track(
self.sheet.master_group, 0,
score_track.ScoreTrack(name='Test'))
self.sheet.master_group.tracks[0].instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=0'
@ -64,9 +66,10 @@ class DeleteTrackTest(SheetCommandTest):
def test_delete_nested_track(self):
grp = track_group.TrackGroup(name='TestGroup')
self.sheet.master_group.tracks.append(grp)
self.sheet.add_track(self.sheet.master_group, 0, grp)
track = score_track.ScoreTrack(name='Test')
grp.tracks.append(track)
self.sheet.add_track(grp, 0, track)
cmd = sheet.RemoveTrack(track_id=track.id)
self.project.dispatch_command(self.sheet.id, cmd)

View File

@ -11,7 +11,7 @@ from . import time_signature
class TimeMapperTest(unittest.TestCase):
def setUp(self):
self.project = project.BaseProject()
self.sheet = sheet.Sheet(name='Test', num_tracks=0)
self.sheet = sheet.Sheet(name='Test')
self.project.sheets.append(self.sheet)
assert len(self.sheet.property_track.measure_list) == 1

View File

@ -20,3 +20,4 @@ from .mutations import (
AddNodeDescription,
RemoveNodeDescription,
)
from .process_base import NodeDBProcessBase

View File

@ -30,6 +30,10 @@ class NodeDBClientMixin(object):
self.server.add_command_handler(
'NODEDB_MUTATION', self.handle_mutation)
async def cleanup(self):
await self.disconnect()
await super().cleanup()
async def connect(self, address, flags=None):
assert self._stub is None
self._stub = ipc.Stub(self.event_loop, address)

View File

@ -66,7 +66,7 @@ class NodeDBClientTest(asynctest.TestCase):
await self.process.cleanup()
async def test_start_scan(self):
await self.client.start_scan()
pass #await self.client.start_scan()
if __name__ == '__main__':

View File

@ -9,6 +9,7 @@ from noisicaa import core
from noisicaa.core import ipc
from .private import db
from . import process_base
logger = logging.getLogger(__name__)
@ -22,34 +23,18 @@ class Session(object):
self.callback_stub = callback_stub
self.flags = flags or set()
self.id = uuid.uuid4().hex
self.pending_mutations = []
def cleanup(self):
pass
async def cleanup(self):
if self.callback_stub is not None:
await self.callback_stub.close()
self.callback_stub = None
def publish_mutation(self, mutation):
if not self.callback_stub.connected:
self.pending_mutations.append(mutation)
return
callback_task = self.event_loop.create_task(
self.callback_stub.call('NODEDB_MUTATION', mutation))
callback_task.add_done_callback(self.publish_mutation_done)
def publish_mutation_done(self, callback_task):
assert callback_task.done()
exc = callback_task.exception()
if exc is not None:
logger.error(
"NODEDB_MUTATION failed with exception: %s", exc)
def callback_stub_connected(self):
async def publish_mutation(self, mutation):
assert self.callback_stub.connected
while self.pending_mutations:
self.publish_mutation(self.pending_mutations.pop(0))
await self.callback_stub.call('NODEDB_MUTATION', mutation)
class NodeDBProcessMixin(object):
class NodeDBProcessMixin(process_base.NodeDBProcessBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sessions = {}
@ -61,14 +46,6 @@ class NodeDBProcessMixin(object):
self._shutting_down = asyncio.Event()
self._shutdown_complete = asyncio.Event()
self.server.add_command_handler(
'START_SESSION', self.handle_start_session)
self.server.add_command_handler(
'END_SESSION', self.handle_end_session)
self.server.add_command_handler('SHUTDOWN', self.handle_shutdown)
self.server.add_command_handler(
'START_SCAN', self.handle_start_scan)
self.db.setup()
async def cleanup(self):
@ -90,33 +67,22 @@ class NodeDBProcessMixin(object):
for session in self.sessions.values():
session.publish_mutation(mutation)
def handle_start_session(self, client_address, flags):
async def handle_start_session(self, client_address, flags):
client_stub = ipc.Stub(self.event_loop, client_address)
connect_task = self.event_loop.create_task(client_stub.connect())
await client_stub.connect()
session = Session(self.event_loop, client_stub, flags)
connect_task.add_done_callback(
functools.partial(self._client_connected, session))
self.sessions[session.id] = session
# Send initial mutations to build up the current pipeline
# state.
for mutation in self.db.initial_mutations():
session.publish_mutation(mutation)
await session.publish_mutation(mutation)
return session.id
def _client_connected(self, session, connect_task):
assert connect_task.done()
exc = connect_task.exception()
if exc is not None:
logger.error("Failed to connect to callback client: %s", exc)
return
session.callback_stub_connected()
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]
async def handle_shutdown(self):

View File

@ -0,0 +1,26 @@
#!/usr/bin/python3
class NodeDBProcessBase(object):
async def setup(self):
await super().setup()
self.server.add_command_handler(
'START_SESSION', self.handle_start_session)
self.server.add_command_handler(
'END_SESSION', self.handle_end_session)
self.server.add_command_handler('SHUTDOWN', self.handle_shutdown)
self.server.add_command_handler(
'START_SCAN', self.handle_start_scan)
def handle_start_session(self, client_address, flags):
raise NotImplementedError
def handle_end_session(self, session_id):
raise NotImplementedError
async def handle_shutdown(self):
raise NotImplementedError
async def handle_start_scan(self, session_id):
raise NotImplementedError

View File

@ -1,480 +0,0 @@
#!/usr/bin/python3
from fractions import Fraction
import logging
import enum
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from .misc import QGraphicsGroup
from . import layout
logger = logging.getLogger(__name__)
class Layer(enum.IntEnum):
BG = 0
MAIN = 1
DEBUG = 2
EDIT = 3
MOUSE = 4
EVENTS = 5
NUM_LAYERS = 6
class TrackItemImpl(object):
def __init__(self, sheet_view, track, **kwargs):
super().__init__(**kwargs)
self._sheet_view = sheet_view
self._track = track
self._layout = None
self._listeners = [
self._track.listeners.add('name', self.onNameChanged),
self._track.listeners.add('muted', self.onMutedChanged),
self._track.listeners.add('volume', self.onVolumeChanged),
self._track.listeners.add('visible', self.onVisibleChanged),
]
def close(self):
for listener in self._listeners:
listener.remove()
@property
def track(self):
return self._track
def onNameChanged(self, old_name, new_name):
# TODO: only update the first measure.
self._sheet_view.updateSheet()
def onMutedChanged(self, old_value, new_value):
pass # TODO
def onVolumeChanged(self, old_value, new_value):
pass # TODO
def onVisibleChanged(self, old_value, new_value):
self._sheet_view.updateSheet()
def getLayout(self):
raise NotImplementedError
def renderTrack(self, y, track_layout):
self._layout = track_layout
def buildContextMenu(self, menu):
track_properties_action = QtWidgets.QAction(
"Edit track properties...", menu,
statusTip="Edit the properties of this track.",
triggered=self.onTrackProperties)
menu.addAction(track_properties_action)
remove_track_action = QtWidgets.QAction(
"Remove track", menu,
statusTip="Remove this track.",
triggered=self.onRemoveTrack)
menu.addAction(remove_track_action)
def onRemoveTrack(self):
self.send_command_async(
self._track.parent.id, 'RemoveTrack',
track=self._track.index)
def onTrackProperties(self):
dialog = QtWidgets.QDialog()
dialog.setWindowTitle("Track Properties")
name = QtWidgets.QLineEdit(dialog)
name.setText(self._track.name)
form_layout = QtWidgets.QFormLayout()
form_layout.addRow("Name", name)
close = QtWidgets.QPushButton("Close")
close.clicked.connect(dialog.close)
buttons = QtWidgets.QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(close)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(form_layout)
layout.addLayout(buttons)
dialog.setLayout(layout)
ret = dialog.exec_()
self.send_command_async(
self._track.id, 'UpdateTrackProperties',
name=name.text())
class MeasureLayout(object):
def __init__(self, duration):
self.duration = duration
self.size = QtCore.QSize()
self.baseline = 0
@property
def is_valid(self):
return self.width > 0 and self.height > 0
@property
def width(self):
return self.size.width()
@width.setter
def width(self, value):
self.size.setWidth(value)
@property
def height(self):
return self.size.height()
@height.setter
def height(self, value):
self.size.setHeight(value)
@property
def extend_above(self):
return self.baseline
@property
def extend_below(self):
return self.height - self.baseline
def __eq__(self, other):
assert isinstance(other, MeasureLayout)
return (self.size == other.size) and (self.baseline == other.baseline)
class MeasureItemImpl(QtWidgets.QGraphicsObject):
def __init__(self, sheet_view, track_item, measure_reference, **kwargs):
super().__init__(**kwargs)
self._sheet_view = sheet_view
self._track_item = track_item
self._measure_reference = measure_reference
if self._measure_reference is not None:
self._measure = measure_reference.measure
self._measure_listener = self._measure_reference.listeners.add(
'measure_id', self.measureChanged)
else:
self._measure = None
self._measure_listener = None
self._layout = None
self._layers = {}
self._layers[Layer.BG] = QGraphicsGroup()
self._background = QtWidgets.QGraphicsRectItem(self._layers[Layer.BG])
self._background.setPen(QtGui.QPen(Qt.NoPen))
self._background.setBrush(QtGui.QColor(240, 240, 255))
self._background.setVisible(False)
self._selected = False
self.setAcceptHoverEvents(True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
@property
def measure(self):
return self._measure
@property
def measure_reference(self):
return self._measure_reference
@property
def track_item(self):
return self._track_item
def boundingRect(self):
return QtCore.QRectF(0, 0, self._layout.width, self._layout.height)
def paint(self, painter, option, widget=None):
pass
def close(self):
if self._measure_listener is not None:
self._measure_listener.remove()
def hoverEnterEvent(self, event):
super().hoverEnterEvent(event)
self.setFocus()
def measureChanged(self, old_value, new_value):
self._measure = self._measure_reference.measure
self.recomputeLayout()
def recomputeLayout(self):
self._sheet_view.scheduleCallback(
'%s:recomputeLayout' % id(self),
self._recomputeLayoutInternal)
def _recomputeLayoutInternal(self):
layout = self.getLayout()
if layout != self._layout:
self._sheet_view.updateSheet()
else:
self.updateMeasure()
def getLayout(self):
raise NotImplementedError
def setLayout(self, layout):
self._layout = layout
def updateMeasure(self):
self._sheet_view.scheduleCallback(