Open capital-G opened 1 year ago
Currently deep in reimplementing Supriya's internals, so a bit of a vague response...
I'm not opposed to implementing something in the spirit JITlib, but I need to understand much more concretely what it's actually doing.
Some of the behavior concerns treating various types of entities interchangeably (synths, patterns, functions) and providing means of seamlessly connecting and crossfading between them. Other parts of the behavior seem to be UX affordances for live coding: how can we allow users to type as little as possible in a live-coding situation to create maximum impact?
An implementation in Supriya starts by teasing those two categories apart through a close reading of the code, with a couple principles in mind:
Basically, we have to implement what's under the hood, as verbose as it needs to be in order to be clear, before we think about whether we want user-level code to use context managers, or subscripting, or magically converting name bindings to JITlib operations.
FWIW, my gut sense is that subscripting is the winner because it's the least implicit, assumes no global state, doesn't add magic or additional methods to SynthDefs or UGens, and will play the best with Python's typing tools. Supriya's never going to be as "pithy" as sclang; that's just not one of my guiding principles 😅
Some of the behavior concerns treating various types of entities interchangeably (synths, patterns, functions) and providing means of seamlessly connecting and crossfading between them. Other parts of the behavior seem to be UX affordances for live coding: how can we allow users to type as little as possible in a live-coding situation to create maximum impact?
Yes, this is indeed just some of the behavior, which happened to be useful. It is a consequence of not having to decide in advance.
One of the main things that may not be apparent immediately is being able to refactor temporal co-dependencies on the fly. It removes the classification of inputs and outputs, and before and after.
Here is a paper that describes the core ideas: http://wertlos.org/~rohrhuber/articles/Algorithms_Today.pdf
Thanks @telephon, that's very helpful 🤔
@capital-G Are you still interested in pursuing this? The big refactor just wrapped up in #323 (although documentation is still in progress), so new features are unblocked.
Hey @josiah-wolf-oberholtzer, thanks for the reminder. Yes, I think NodeProxies would be a great addition to supriya as they are me my preferred "dialect" and it would be interesting what Python enables in regards of live coding (e.g. having dynamic classes, language server with inspection, network access and the whole ecosystem etc.).
I mainly have 2 questions:
I can imagine something like this going into Supriya eventually. Unlike GUI stuff, there's no equivalent to JITlib in the Python ecosystem, so might make sense to keep it close. Try forking, open a PR, and we can take the conversation from there.
I know I don't want to recapitulate the same interfaces, names, or (often) design patterns I see in sclang. A lot of code started in Supriya mirroring sclang's interfaces and semantics, but has changed considerably over the last ~decade. If I were doing this myself I would study the original code, but start designing in Python with a tabula rasa. There are some general principles over here that are going to force API decisions for anything new coming into the project: no global state (no default servers etc.), avoid implicit behavior, use context managers if implicit behavior is unavoidable, provide verbose explicit methods for anything that also uses fancy syntax tricks, etc.
Cheers!
It I were you, I'd try to implement the core ideas:
Let's talk through API design / implementation a little bit.
Here's the example at the top of the original ProxySpace documentation:
s.boot;
p = ProxySpace.new;
p.fadeTime = 2; // fadeTime specifies crossfade
p[\out].play; // monitor an empty placeholder through hardware output
// set its source
p[\out] = { SinOsc.ar([350, 351.3], 0, 0.2) };
p[\out] = { Pulse.ar([350, 351.3] / 4, 0.4) * 0.2 };
p[\out] = Pbind(\dur, 0.03, \freq, Pbrown(0, 1, 0.1, inf).linexp(0, 1, 200, 350));
// route one proxy through another:
p[\out] = { Ringz.ar(p[\in].ar, [350, 351.3] * 8, 0.2) * 4 };
p[\in] = { Impulse.ar([5, 7]/2, [0, 0.5]) };
p.clear(3); // clear after 3 seconds
And here's what I want this to look like in Python:
s = Server().boot() # There's no default server in Supriya, by design
p = ProxySpace(s) # 1) ProxySpace needs an explicit reference to a server
p.fade_time = 2
p["out"].play()
with p["out"]: # 2) SynthDefs should be built inside a context manager
SinOsc.ar(frequency=[350, 351.3]) * 0.2
with p["out"]:
Pulse.ar(frequency=[350 / 4, 351.3 / 4], width=0.4) * 0.2
# 3) Let's ignore patterns for the initial feature
p["out"] = EventPattern(
duration=0.03,
frequency=RandomPattern(distribution="BROWN"),
).scale(0, 1, 200, 350, exponential=True)
# 4) Referencing proxies as SynthDef parameter should use a parameter factory,
# not the NodeProxies directly
with p["out"] as psp:
Ringz.ar(source=psp["in"].ar(), frequency=[350, 351.3] * 8, decay_time=0.2) * 4
with p["in"]:
Impulse.ar(frequency=[5 / 2, 7 / 2], phase=[0, 0.5])
p.clear(3)
Yes, this is a little more verbose, but we're never going to be as concise as sclang. That's not one of my goals.
I'll expand on the numbered points in the comments for my Python version.
This one's easy. There's no global / default anything in Supriya - everything needs to be referenced explicitly - so the ProxySpace needs an explicit reference to the server you're intending to use. There used to be a "default" server, but I backed that out a few years ago.
SynthDefs need to be built inside a context manager's runtime context.
Python doesn't have curly braces or multi-line lambdas, so I use a
SynthDefBuilder context manager for collecting UGens into a Synthdef. This is
true even with Supriya's @synthdef function decorator, just hidden from sight.
That means NodeProxies need to implement __enter__
and __exit__
methods
for entering and exiting a runtime context. Internally those methods can setup
a SynthDefBuilder object, enter its context and then exit it to collect and
construct the SynthDef we'll use for the NodeProxy.
class NodeProxy:
def __enter__(self) -> ProxySpaceParameterFactory:
# create and save a reference to a synthdef builder
self._synthdef_builder = SynthDefBuilder()
# enter the synthdef builder's context
self.-synthdef_builder.__enter__()
# create and return a parameter factory
return ProxySpaceParameterFactory(self.proxy_space)
def __exit__(self, *args) -> None:
# exit the synthdef builder's runtime context
self._synthdef_builder.__exit__()
# build the synthdef with the collected ugens
synthdef = self._synthdef_builder.build()
# do whatever it is you do
... do stuff with the synthdef
We can talk about relaxing this restriction once the initial feature is done - I'm sure the draw of saving a few keystrokes by just assigning a UGen to the ProxySpace key is powerful - but I'd prefer to begin strict and relax later rather than the reverse.
Let's just ignore patterns for the MVP. While Supriya does have a pattern
system, it only implements a subset of what sclang currently has, and that
doesn't yet include Pbrown
or .linexp
. There's also the problem of
setting up clocks. As in point 1) above, there's no default clock and there's
not going to be, so any clock will either be created with the ProxySpace or
passed in alongside the server argument. We'll cross this bridge later.
NodeProxies should not be used directly to build UGen graphs. There's a few reasons for this.
One, treating a NodeProxy as a UGen-compatible object means giving
NodeProxy the entire interface exposed on Supriya's
UGenMethodMixin.
That interface includes nearly every operator override already, making those
unusable for NodeProxies outside the context of SynthDef building. While sclang
can invent additional operators (<<>
, <>>
etc.), we can't do that in
Python, so best to reserve them.
Two, even if operator overrides weren't an issue, I still wouldn't want to
conflate NodeProxy and UGen. I want to keep my class interfaces as small as
possible whenever I can. Supriya already has a Parameter class for modeling
SynthDef parameters, let's just hijack that / subclass it for use with
NodeProxies. Note in 2) above that NodeProxy's __enter__
returns something
I'm calling a ProxySpaceParameterFactory. Whatever that class is, it knows
about the ProxySpace, it can be subscripted with the same keys used on the
ProxySpace, but its __getitem__
returns a Parameter (or subclass thereof)
instead of a NodeProxy.
Lemme know if you have any questions!
Is your feature request related to a problem? Please describe.
I enjoy the JITlib approach to sclang with Ndefs, Pdefs etc, so something like this is possible
I checked out supriya really quickly and I did not find anything like this immediately.
The basic idea is that you can not loose a handle to a running synth by overwriting a variable - instead by overwriting a variable you also overwrite/replace it on the server.
Describe the solution you'd like
I still don't have a concise idea how this could be best implemented, I see this issue more as design process or discussion. I think we can change the idea of NodeProxies to be more pythonic, having the best of both worlds.
A quick Idea that came to my mind is by using a subscriptable class (of which one could have multiple or in a recursive/nested manner?), attaching each node as element of a class instead of having independent objects (like in the SC implementation).
I think all the necessary lifting could be done in the
__getitem__
and__setitem__
methods, albeit the (internal) wiring of Ndefs can be really tricky sometimes.An alternative could be looking at ProxySpace in SuperCollider.
The problem of a ProxySpace is that every function will be treated as a NodeProxy, so defining a function like
~myFunc = {"foo".postln}
results in an error as a String is not something used on the synth. Using Python I don't see a problem in this case.One could use e.g. a context manager for proxyspace
Or maybe make a tabula rasa and create something pythonic from the ground up?
I would really enjoy contributing to this project - maybe we can have some discussion here how to implement this and then I can do a first draft? :)
Additional context
Kudos to the programming style!