Skip to content

Commit

Permalink
Integrating complex_session fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
josephine-wolf-oberholtzer committed Dec 15, 2024
1 parent aecc0b8 commit 6763c5d
Show file tree
Hide file tree
Showing 7 changed files with 839 additions and 424 deletions.
20 changes: 11 additions & 9 deletions supriya/mixers/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,18 +307,20 @@ def _get_control_bus(
name=name,
)

async def dump_tree(self) -> QueryTreeGroup:
async def dump_tree(self, annotated: bool = True) -> QueryTreeGroup:
if self.session and self.session.status != BootStatus.ONLINE:
raise RuntimeError
annotations: Dict[int, str] = {}
tree = await cast(
Awaitable[QueryTreeGroup],
cast(Group, self._nodes[ComponentNames.GROUP]).dump_tree(),
)
for component in self._walk():
if not isinstance(component, AllocatableComponent):
continue
address = component.address
for name, node in component._nodes.items():
annotations[node.id_] = f"{address}:{name}"
return tree.annotate(annotations)
if annotated:
annotations: Dict[int, str] = {}
for component in self._walk():
if not isinstance(component, AllocatableComponent):
continue
address = component.address
for name, node in component._nodes.items():
annotations[node.id_] = f"{address}:{name}"
return tree.annotate(annotations)
return tree
37 changes: 19 additions & 18 deletions supriya/mixers/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(
def _allocate(self, *, context: AsyncServer) -> bool:
if not super()._allocate(context=context):
return False
return self._reconcile(context)
return self._reconcile(context=context)

def _allocate_synth(
self,
Expand All @@ -76,21 +76,24 @@ def _allocate_synth(
synthdef=PATCH_CABLE_2_2,
)

def _delete(self) -> None:
super()._delete()
for component in [
self._cached_state.source_component,
self._cached_state.target_component,
]:
if component:
component._unregister_dependency(self)
def _deallocate(self) -> None:
super()._deallocate()
# NOTE: Resetting dependencies and state guarantees that the component-
# and node-tree reallocates idempotently on session reboot. In
# practice, this doesn't matter, but it does ensure the test
# suite doesn't need to special case node or bus IDs.
self._reconcile(new_state=self.State())

def _get_synthdefs(self) -> List[SynthDef]:
return [FB_PATCH_CABLE_2_2, PATCH_CABLE_2_2]

def _reconcile(self, context: Optional[AsyncServer] = None) -> bool:
new_state = self._resolve_state(context)
self._reconcile_dependencies(context, new_state)
def _reconcile(
self,
context: Optional[AsyncServer] = None,
new_state: Optional["Connection.State"] = None,
) -> bool:
new_state = new_state or self._resolve_state(context)
self._reconcile_dependencies(new_state)
self._reconcile_synth(context, new_state)
self._cached_state = new_state
return self._reconcile_deferment(new_state)
Expand All @@ -104,9 +107,7 @@ def _reconcile_deferment(self, new_state: "Connection.State") -> bool:
return False
return True

def _reconcile_dependencies(
self, context: Optional[AsyncServer], new_state: "Connection.State"
) -> None:
def _reconcile_dependencies(self, new_state: "Connection.State") -> None:
for new_component, old_component in [
(new_state.source_component, self._cached_state.source_component),
(new_state.target_component, self._cached_state.target_component),
Expand Down Expand Up @@ -215,19 +216,19 @@ def _resolve_target(

def _set_postfader(self, postfader: bool) -> None:
self._postfader = postfader
self._reconcile(self._can_allocate())
self._reconcile(context=self._can_allocate())

def _set_source(self, source: Optional[S]) -> None:
if isinstance(source, AllocatableComponent) and self.mixer is not source.mixer:
raise RuntimeError
self._source = source
self._reconcile(self._can_allocate())
self._reconcile(context=self._can_allocate())

def _set_target(self, target: Optional[T]) -> None:
if isinstance(target, AllocatableComponent) and self.mixer is not target.mixer:
raise RuntimeError
self._target = target
self._reconcile(self._can_allocate())
self._reconcile(context=self._can_allocate())

@classmethod
def feedsback(
Expand Down
31 changes: 27 additions & 4 deletions supriya/mixers/sessions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Set
import re
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Set, Union

from ..clocks import AsyncClock
from ..contexts import AsyncServer
Expand Down Expand Up @@ -47,6 +48,25 @@ def __init__(self) -> None:
self._contexts[(context := self._add_context())] = [mixer := Mixer(parent=self)]
self._mixers[mixer] = context

def __getitem__(self, key: str) -> "Component":
if not isinstance(key, str):
raise ValueError(key)
elif not re.match(r"^[a-z_]+(\[\d+\])?(\.[a-z_]+(\[\d+\])?)*$", key):
raise ValueError(key)
item: Union[Component, Sequence[Component]] = self
for part in key.split("."):
if not (match := re.match(r"^([a-z_]+)(\[(\d+)\])?$", part)):
raise ValueError(key, part)
name, _, index = match.groups()
item = getattr(item, name)
if index is not None:
if not isinstance(item, Sequence):
raise ValueError(item, index)
item = item[int(index)]
if isinstance(item, Sequence):
raise ValueError(item)
return item

def __repr__(self) -> str:
return f"<{type(self).__name__}>"

Expand Down Expand Up @@ -95,6 +115,9 @@ async def add_mixer(self, context: Optional[AsyncServer] = None) -> "Mixer":

async def boot(self) -> None:
async with self._lock:
# reset state
for set_ in self._synthdefs.values():
set_.clear()
# guard against concurrent boot / quits
if self._status == BootStatus.OFFLINE:
self._quit_future = None
Expand Down Expand Up @@ -122,7 +145,7 @@ async def delete_context(self, context: AsyncServer) -> None:
async def dump_components(self) -> str:
return ""

async def dump_tree(self) -> str:
async def dump_tree(self, annotated: bool = True) -> str:
# what if components and query tree stuff was intermixed?
# we fetch the node tree once per mixer
# and then the node tree needs to get partitioned by subtrees
Expand All @@ -132,7 +155,7 @@ async def dump_tree(self) -> str:
for context, mixers in self._contexts.items():
parts.append(repr(context))
for mixer in mixers:
for line in str(await mixer.dump_tree()).splitlines():
for line in str(await mixer.dump_tree(annotated)).splitlines():
parts.append(f" {line}")
return "\n".join(parts)

Expand All @@ -146,7 +169,7 @@ async def quit(self) -> None:
for context, mixers in self._contexts.items():
for mixer in mixers:
with context.at():
mixer._deallocate()
mixer._deallocate_deep()
await asyncio.gather(*[context.quit() for context in self._contexts])
self._status = BootStatus.OFFLINE
self._quit_future.set_result(True)
Expand Down
6 changes: 4 additions & 2 deletions supriya/mixers/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,9 @@ async def move(self, parent: TrackContainer, index: int) -> None:
raise RuntimeError
elif self in parent.parentage:
raise RuntimeError
elif index < 0 or index > len(parent.tracks):
elif index < 0:
raise RuntimeError
elif index and index >= len(parent.tracks):
raise RuntimeError
# Reconfigure parentage and bail if this is a no-op
old_parent, old_index = self._parent, 0
Expand All @@ -370,7 +372,7 @@ async def move(self, parent: TrackContainer, index: int) -> None:
self._nodes[ComponentNames.GROUP].move(
target_node=node_id, add_action=add_action
)
for component in self._dependents:
for component in sorted(self._dependents, key=lambda x: x.graph_order):
component._reconcile(context)

async def set_active(self, active: bool = True) -> None:
Expand Down
148 changes: 143 additions & 5 deletions tests/mixers/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import contextlib
import difflib
from typing import List, Optional, Union
from typing import List, Optional, Tuple, Union

import pytest
import pytest_asyncio
from uqbar.strings import normalize

from supriya import AsyncServer, OscBundle, OscMessage
Expand All @@ -22,20 +23,25 @@ def capture(context: Optional[AsyncServer]):
entries.extend(transcript.filtered(received=False, status=False))


async def debug_tree(session: Session, label: str = "initial tree") -> str:
tree = str(await session.dump_tree())
async def debug_tree(
session: Session, label: str = "initial tree", annotated: bool = True
) -> str:
tree = str(await session.dump_tree(annotated=annotated))
for i, context in enumerate(session.contexts):
tree = tree.replace(repr(context), f"<session.contexts[{i}]>")
print(f"{label}:\n{tree}")
return tree


async def assert_diff(
session: Session, expected_diff: str, expected_initial_tree: str
session: Session,
expected_diff: str,
expected_initial_tree: str,
annotated: bool = True,
) -> None:
await session.sync()
print(f"expected initial tree:\n{normalize(expected_initial_tree)}")
actual_tree = await debug_tree(session, "actual tree")
actual_tree = await debug_tree(session, "actual tree", annotated=annotated)
actual_diff = "".join(
difflib.unified_diff(
normalize(expected_initial_tree).splitlines(True),
Expand Down Expand Up @@ -64,3 +70,135 @@ def session() -> Session:
@pytest.fixture
def track(mixer: Mixer) -> Track:
return mixer.tracks[0]


@pytest_asyncio.fixture
async def complex_session() -> Tuple[Session, str]:
session = Session()
await session.add_mixer()
mixer_one = session.mixers[0]
# tracks
track_one = mixer_one.tracks[0] # track_one
track_two = await mixer_one.add_track() # track_two
await mixer_one.add_track() # track_three
track_one_one = await track_one.add_track() # track_one_one
await track_one.add_track() # track_one_two
await track_one_one.add_track() # track_one_one_one
# add sends
await track_one.add_send(track_two)
await track_two.add_send(track_one_one)
# record initial tree
await session.boot()
initial_tree = await debug_tree(session)
assert initial_tree == normalize(
"""
<session.contexts[0]>
NODE TREE 1000 group (session.mixers[0]:group)
1001 group (session.mixers[0]:tracks)
1006 group (session.mixers[0].tracks[0]:group)
1007 group (session.mixers[0].tracks[0]:tracks)
1012 group (session.mixers[0].tracks[0].tracks[0]:group)
1041 supriya:fb-patch-cable:2x2 (session.mixers[0].tracks[0].tracks[0].feedback:synth)
active: c11, gain: 0.0, gate: 1.0, in_: 28.0, out: 20.0
1013 group (session.mixers[0].tracks[0].tracks[0]:tracks)
1018 group (session.mixers[0].tracks[0].tracks[0].tracks[0]:group)
1019 group (session.mixers[0].tracks[0].tracks[0].tracks[0]:tracks)
1022 supriya:meters:2 (session.mixers[0].tracks[0].tracks[0].tracks[0]:input-levels)
in_: 22.0, out: 19.0
1020 group (session.mixers[0].tracks[0].tracks[0].tracks[0]:devices)
1021 supriya:channel-strip:2 (session.mixers[0].tracks[0].tracks[0].tracks[0]:channel-strip)
active: c17, bus: 22.0, gain: c18, gate: 1.0
1023 supriya:meters:2 (session.mixers[0].tracks[0].tracks[0].tracks[0]:output-levels)
in_: 22.0, out: 21.0
1024 supriya:patch-cable:2x2 (session.mixers[0].tracks[0].tracks[0].tracks[0].output:synth)
active: c17, gain: 0.0, gate: 1.0, in_: 22.0, out: 20.0
1016 supriya:meters:2 (session.mixers[0].tracks[0].tracks[0]:input-levels)
in_: 20.0, out: 13.0
1014 group (session.mixers[0].tracks[0].tracks[0]:devices)
1015 supriya:channel-strip:2 (session.mixers[0].tracks[0].tracks[0]:channel-strip)
active: c11, bus: 20.0, gain: c12, gate: 1.0
1017 supriya:meters:2 (session.mixers[0].tracks[0].tracks[0]:output-levels)
in_: 20.0, out: 15.0
1025 supriya:patch-cable:2x2 (session.mixers[0].tracks[0].tracks[0].output:synth)
active: c11, gain: 0.0, gate: 1.0, in_: 20.0, out: 18.0
1026 group (session.mixers[0].tracks[0].tracks[1]:group)
1027 group (session.mixers[0].tracks[0].tracks[1]:tracks)
1030 supriya:meters:2 (session.mixers[0].tracks[0].tracks[1]:input-levels)
in_: 24.0, out: 25.0
1028 group (session.mixers[0].tracks[0].tracks[1]:devices)
1029 supriya:channel-strip:2 (session.mixers[0].tracks[0].tracks[1]:channel-strip)
active: c23, bus: 24.0, gain: c24, gate: 1.0
1031 supriya:meters:2 (session.mixers[0].tracks[0].tracks[1]:output-levels)
in_: 24.0, out: 27.0
1032 supriya:patch-cable:2x2 (session.mixers[0].tracks[0].tracks[1].output:synth)
active: c23, gain: 0.0, gate: 1.0, in_: 24.0, out: 18.0
1010 supriya:meters:2 (session.mixers[0].tracks[0]:input-levels)
in_: 18.0, out: 7.0
1008 group (session.mixers[0].tracks[0]:devices)
1009 supriya:channel-strip:2 (session.mixers[0].tracks[0]:channel-strip)
active: c5, bus: 18.0, gain: c6, gate: 1.0
1051 supriya:patch-cable:2x2 (session.mixers[0].tracks[0].sends[0]:synth)
active: c5, gain: 0.0, gate: 1.0, in_: 18.0, out: 26.0
1011 supriya:meters:2 (session.mixers[0].tracks[0]:output-levels)
in_: 18.0, out: 9.0
1033 supriya:patch-cable:2x2 (session.mixers[0].tracks[0].output:synth)
active: c5, gain: 0.0, gate: 1.0, in_: 18.0, out: 16.0
1034 group (session.mixers[0].tracks[1]:group)
1035 group (session.mixers[0].tracks[1]:tracks)
1038 supriya:meters:2 (session.mixers[0].tracks[1]:input-levels)
in_: 26.0, out: 31.0
1036 group (session.mixers[0].tracks[1]:devices)
1037 supriya:channel-strip:2 (session.mixers[0].tracks[1]:channel-strip)
active: c29, bus: 26.0, gain: c30, gate: 1.0
1042 supriya:patch-cable:2x2 (session.mixers[0].tracks[1].sends[0]:synth)
active: c29, gain: 0.0, gate: 1.0, in_: 26.0, out: 28.0
1039 supriya:meters:2 (session.mixers[0].tracks[1]:output-levels)
in_: 26.0, out: 33.0
1040 supriya:patch-cable:2x2 (session.mixers[0].tracks[1].output:synth)
active: c29, gain: 0.0, gate: 1.0, in_: 26.0, out: 16.0
1043 group (session.mixers[0].tracks[2]:group)
1044 group (session.mixers[0].tracks[2]:tracks)
1047 supriya:meters:2 (session.mixers[0].tracks[2]:input-levels)
in_: 30.0, out: 37.0
1045 group (session.mixers[0].tracks[2]:devices)
1046 supriya:channel-strip:2 (session.mixers[0].tracks[2]:channel-strip)
active: c35, bus: 30.0, gain: c36, gate: 1.0
1048 supriya:meters:2 (session.mixers[0].tracks[2]:output-levels)
in_: 30.0, out: 39.0
1049 supriya:patch-cable:2x2 (session.mixers[0].tracks[2].output:synth)
active: c35, gain: 0.0, gate: 1.0, in_: 30.0, out: 16.0
1004 supriya:meters:2 (session.mixers[0]:input-levels)
in_: 16.0, out: 1.0
1002 group (session.mixers[0]:devices)
1003 supriya:channel-strip:2 (session.mixers[0]:channel-strip)
active: 1.0, bus: 16.0, gain: c0, gate: 1.0
1005 supriya:meters:2 (session.mixers[0]:output-levels)
in_: 16.0, out: 3.0
1050 supriya:patch-cable:2x2 (session.mixers[0].output:synth)
active: 1.0, gain: 0.0, gate: 1.0, in_: 16.0, out: 0.0
NODE TREE 1052 group (session.mixers[1]:group)
1053 group (session.mixers[1]:tracks)
1058 group (session.mixers[1].tracks[0]:group)
1059 group (session.mixers[1].tracks[0]:tracks)
1062 supriya:meters:2 (session.mixers[1].tracks[0]:input-levels)
in_: 34.0, out: 48.0
1060 group (session.mixers[1].tracks[0]:devices)
1061 supriya:channel-strip:2 (session.mixers[1].tracks[0]:channel-strip)
active: c46, bus: 34.0, gain: c47, gate: 1.0
1063 supriya:meters:2 (session.mixers[1].tracks[0]:output-levels)
in_: 34.0, out: 50.0
1064 supriya:patch-cable:2x2 (session.mixers[1].tracks[0].output:synth)
active: c46, gain: 0.0, gate: 1.0, in_: 34.0, out: 32.0
1056 supriya:meters:2 (session.mixers[1]:input-levels)
in_: 32.0, out: 42.0
1054 group (session.mixers[1]:devices)
1055 supriya:channel-strip:2 (session.mixers[1]:channel-strip)
active: 1.0, bus: 32.0, gain: c41, gate: 1.0
1057 supriya:meters:2 (session.mixers[1]:output-levels)
in_: 32.0, out: 44.0
1065 supriya:patch-cable:2x2 (session.mixers[1].output:synth)
active: 1.0, gain: 0.0, gate: 1.0, in_: 32.0, out: 0.0
"""
)
await session.quit()
return session, initial_tree
Loading

0 comments on commit 6763c5d

Please sign in to comment.