Skip to content

Commit 5028395

Browse files
Implement mixers
1 parent fd1ca1b commit 5028395

18 files changed

+4530
-0
lines changed

supriya/mixers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .sessions import Session
2+
3+
__all__ = ["Session"]

supriya/mixers/components.py

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import asyncio
2+
from typing import (
3+
TYPE_CHECKING,
4+
Awaitable,
5+
Dict,
6+
Generator,
7+
Generic,
8+
Iterator,
9+
List,
10+
Literal,
11+
Optional,
12+
Set,
13+
Tuple,
14+
Type,
15+
TypeVar,
16+
cast,
17+
)
18+
19+
try:
20+
from typing import TypeAlias
21+
except ImportError:
22+
from typing_extensions import TypeAlias # noqa
23+
24+
from ..contexts import AsyncServer, Buffer, BusGroup, Context, Group, Node
25+
from ..contexts.responses import QueryTreeGroup
26+
from ..enums import BootStatus, CalculationRate
27+
from ..ugens import SynthDef
28+
from ..utils import iterate_nwise
29+
30+
C = TypeVar("C", bound="Component")
31+
32+
A = TypeVar("A", bound="AllocatableComponent")
33+
34+
# TODO: Integrate this with channel logic
35+
ChannelCount: TypeAlias = Literal[1, 2, 4, 8]
36+
37+
if TYPE_CHECKING:
38+
from .mixers import Mixer
39+
from .sessions import Session
40+
41+
42+
class ComponentNames:
43+
ACTIVE = "active"
44+
CHANNEL_STRIP = "channel-strip"
45+
DEVICES = "devices"
46+
FEEDBACK = "feedback"
47+
GAIN = "gain"
48+
GROUP = "group"
49+
INPUT = "input"
50+
INPUT_LEVELS = "input-levels"
51+
MAIN = "main"
52+
OUTPUT = "output"
53+
OUTPUT_LEVELS = "output-levels"
54+
SYNTH = "synth"
55+
TRACKS = "tracks"
56+
57+
58+
class Component(Generic[C]):
59+
60+
def __init__(
61+
self,
62+
*,
63+
parent: Optional[C] = None,
64+
) -> None:
65+
self._lock = asyncio.Lock()
66+
self._parent: Optional[C] = parent
67+
self._dependents: Set[Component] = set()
68+
self._is_active = True
69+
self._feedback_dependents: Set[Component] = set()
70+
71+
def __repr__(self) -> str:
72+
return f"<{type(self).__name__}>"
73+
74+
async def _allocate_deep(self, *, context: AsyncServer) -> None:
75+
if self.session is None:
76+
raise RuntimeError
77+
fifo: List[Tuple[Component, int]] = []
78+
current_synthdefs = self.session._synthdefs[context]
79+
desired_synthdefs: Set[SynthDef] = set()
80+
for component in self._walk():
81+
fifo.append((component, 0))
82+
desired_synthdefs.update(component._get_synthdefs())
83+
if required_synthdefs := sorted(
84+
desired_synthdefs - current_synthdefs, key=lambda x: x.effective_name
85+
):
86+
for synthdef in required_synthdefs:
87+
context.add_synthdefs(synthdef)
88+
await context.sync()
89+
current_synthdefs.update(required_synthdefs)
90+
while fifo:
91+
component, attempts = fifo.pop(0)
92+
if attempts > 2:
93+
raise RuntimeError(component, attempts)
94+
if not component._allocate(context=context):
95+
fifo.append((component, attempts + 1))
96+
97+
def _allocate(self, *, context: AsyncServer) -> bool:
98+
return True
99+
100+
def _deallocate(self) -> None:
101+
pass
102+
103+
def _deallocate_deep(self) -> None:
104+
for component in self._walk():
105+
component._deallocate()
106+
107+
def _delete(self) -> None:
108+
self._deallocate_deep()
109+
self._parent = None
110+
111+
def _get_synthdefs(self) -> List[SynthDef]:
112+
return []
113+
114+
def _iterate_parentage(self) -> Iterator["Component"]:
115+
component = self
116+
while component.parent is not None:
117+
yield component
118+
component = component.parent
119+
yield component
120+
121+
def _reconcile(self, context: Optional[AsyncServer] = None) -> bool:
122+
return True
123+
124+
def _register_dependency(self, dependent: "Component") -> None:
125+
self._dependents.add(dependent)
126+
127+
def _register_feedback(
128+
self, context: Optional[AsyncServer], dependent: "Component"
129+
) -> Optional[BusGroup]:
130+
self._dependents.add(dependent)
131+
self._feedback_dependents.add(dependent)
132+
return None
133+
134+
def _unregister_dependency(self, dependent: "Component") -> bool:
135+
self._dependents.discard(dependent)
136+
return self._unregister_feedback(dependent)
137+
138+
def _unregister_feedback(self, dependent: "Component") -> bool:
139+
had_feedback = bool(self._feedback_dependents)
140+
self._feedback_dependents.discard(dependent)
141+
return had_feedback and not self._feedback_dependents
142+
143+
def _walk(
144+
self, component_class: Optional[Type["Component"]] = None
145+
) -> Generator["Component", None, None]:
146+
component_class_ = component_class or Component
147+
if isinstance(self, component_class_):
148+
yield self
149+
for child in self.children:
150+
yield from child._walk(component_class_)
151+
152+
@property
153+
def address(self) -> str:
154+
raise NotImplementedError
155+
156+
@property
157+
def children(self) -> List["Component"]:
158+
return []
159+
160+
@property
161+
def context(self) -> Optional[AsyncServer]:
162+
if (mixer := self.mixer) is not None:
163+
return mixer.context
164+
return None
165+
166+
@property
167+
def graph_order(self) -> Tuple[int, ...]:
168+
# TODO: Cache this
169+
graph_order = []
170+
for parent, child in iterate_nwise(reversed(list(self._iterate_parentage()))):
171+
graph_order.append(parent.children.index(child))
172+
return tuple(graph_order)
173+
174+
@property
175+
def mixer(self) -> Optional["Mixer"]:
176+
# TODO: Cache this
177+
from .mixers import Mixer
178+
179+
for component in self._iterate_parentage():
180+
if isinstance(component, Mixer):
181+
return component
182+
return None
183+
184+
@property
185+
def parent(self) -> Optional[C]:
186+
return self._parent
187+
188+
@property
189+
def parentage(self) -> List["Component"]:
190+
# TODO: Cache this
191+
return list(self._iterate_parentage())
192+
193+
@property
194+
def session(self) -> Optional["Session"]:
195+
# TODO: Cache this
196+
from .sessions import Session
197+
198+
for component in self._iterate_parentage():
199+
if isinstance(component, Session):
200+
return component
201+
return None
202+
203+
@property
204+
def short_address(self) -> str:
205+
address = self.address
206+
for from_, to_ in [
207+
("session.", ""),
208+
("tracks", "t"),
209+
("devices", "d"),
210+
("mixers", "m"),
211+
]:
212+
address = address.replace(from_, to_)
213+
return address
214+
215+
216+
class AllocatableComponent(Component[C]):
217+
218+
def __init__(
219+
self,
220+
*,
221+
parent: Optional[C] = None,
222+
) -> None:
223+
super().__init__(parent=parent)
224+
self._audio_buses: Dict[str, BusGroup] = {}
225+
self._buffers: Dict[str, Buffer] = {}
226+
self._context: Optional[Context] = None
227+
self._control_buses: Dict[str, BusGroup] = {}
228+
self._is_active: bool = True
229+
self._nodes: Dict[str, Node] = {}
230+
231+
def _can_allocate(self) -> Optional[AsyncServer]:
232+
if (
233+
context := self.context
234+
) is not None and context.boot_status == BootStatus.ONLINE:
235+
return context
236+
return None
237+
238+
def _deallocate(self) -> None:
239+
super()._deallocate()
240+
for key in tuple(self._audio_buses):
241+
self._audio_buses.pop(key).free()
242+
for key in tuple(self._control_buses):
243+
self._control_buses.pop(key).free()
244+
if group := self._nodes.get(ComponentNames.GROUP):
245+
if not self._is_active:
246+
group.free()
247+
else:
248+
group.set(gate=0)
249+
self._nodes.clear()
250+
for key in tuple(self._buffers):
251+
self._buffers.pop(key).free()
252+
253+
def _get_audio_bus(
254+
self,
255+
context: Optional[AsyncServer],
256+
name: str,
257+
can_allocate: bool = False,
258+
channel_count: int = 2,
259+
) -> BusGroup:
260+
return self._get_buses(
261+
calculation_rate=CalculationRate.AUDIO,
262+
can_allocate=can_allocate,
263+
channel_count=channel_count,
264+
context=context,
265+
name=name,
266+
)
267+
268+
def _get_buses(
269+
self,
270+
context: Optional[AsyncServer],
271+
name: str,
272+
*,
273+
calculation_rate: CalculationRate,
274+
can_allocate: bool = False,
275+
channel_count: int = 1,
276+
) -> BusGroup:
277+
if calculation_rate == CalculationRate.CONTROL:
278+
buses = self._control_buses
279+
elif calculation_rate == CalculationRate.AUDIO:
280+
buses = self._audio_buses
281+
else:
282+
raise ValueError(calculation_rate)
283+
if (name not in buses) and can_allocate and context:
284+
buses[name] = context.add_bus_group(
285+
calculation_rate=calculation_rate,
286+
count=channel_count,
287+
)
288+
return buses[name]
289+
290+
def _get_control_bus(
291+
self,
292+
context: Optional[AsyncServer],
293+
name: str,
294+
can_allocate: bool = False,
295+
channel_count: int = 1,
296+
) -> BusGroup:
297+
return self._get_buses(
298+
calculation_rate=CalculationRate.CONTROL,
299+
can_allocate=can_allocate,
300+
channel_count=channel_count,
301+
context=context,
302+
name=name,
303+
)
304+
305+
async def dump_tree(self, annotated: bool = True) -> QueryTreeGroup:
306+
if self.session and self.session.status != BootStatus.ONLINE:
307+
raise RuntimeError
308+
tree = await cast(
309+
Awaitable[QueryTreeGroup],
310+
cast(Group, self._nodes[ComponentNames.GROUP]).dump_tree(),
311+
)
312+
if annotated:
313+
annotations: Dict[int, str] = {}
314+
for component in self._walk():
315+
if not isinstance(component, AllocatableComponent):
316+
continue
317+
address = component.address
318+
for name, node in component._nodes.items():
319+
annotations[node.id_] = f"{address}:{name}"
320+
return tree.annotate(annotations)
321+
return tree

supriya/mixers/devices.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import List
2+
3+
from ..contexts import AsyncServer
4+
from ..enums import AddAction
5+
from ..ugens import SynthDef
6+
from .components import AllocatableComponent, C, ComponentNames
7+
from .synthdefs import DEVICE_DC_TESTER_2
8+
9+
10+
class DeviceContainer(AllocatableComponent[C]):
11+
12+
def __init__(self) -> None:
13+
self._devices: List[Device] = []
14+
15+
def _delete_device(self, device: "Device") -> None:
16+
self._devices.remove(device)
17+
18+
async def add_device(self) -> "Device":
19+
async with self._lock:
20+
self._devices.append(device := Device(parent=self))
21+
if context := self._can_allocate():
22+
await device._allocate_deep(context=context)
23+
return device
24+
25+
@property
26+
def devices(self) -> List["Device"]:
27+
return self._devices[:]
28+
29+
30+
class Device(AllocatableComponent):
31+
32+
def _allocate(self, *, context: AsyncServer) -> bool:
33+
if not super()._allocate(context=context):
34+
return False
35+
elif self.parent is None:
36+
raise RuntimeError
37+
main_audio_bus = self.parent._get_audio_bus(context, name=ComponentNames.MAIN)
38+
target_node = self.parent._nodes[ComponentNames.DEVICES]
39+
with context.at():
40+
self._nodes[ComponentNames.GROUP] = group = target_node.add_group(
41+
add_action=AddAction.ADD_TO_TAIL
42+
)
43+
self._nodes[ComponentNames.SYNTH] = group.add_synth(
44+
add_action=AddAction.ADD_TO_TAIL,
45+
out=main_audio_bus,
46+
synthdef=DEVICE_DC_TESTER_2,
47+
)
48+
return True
49+
50+
def _get_synthdefs(self) -> List[SynthDef]:
51+
return [DEVICE_DC_TESTER_2]
52+
53+
async def set_active(self, active: bool = True) -> None:
54+
async with self._lock:
55+
pass
56+
57+
@property
58+
def address(self) -> str:
59+
if self.parent is None:
60+
return "devices[?]"
61+
index = self.parent.devices.index(self)
62+
return f"{self.parent.address}.devices[{index}]"

0 commit comments

Comments
 (0)