ccrma / chuck

ChucK Music Programming Language
http://chuck.stanford.edu/
GNU General Public License v2.0
778 stars 126 forks source link

Syntactic Sugar for Patch chugin (aka simple & easy LFOs) #318

Open nshaheed opened 1 year ago

nshaheed commented 1 year ago

This is a potential approach for making lfos hopefully both simpler to use and more aesthetically in-line with chuck as a language. What if making an LFO was this simple?

SinOsc s => dac;
SinOsc lfo => s.phase;

This would ideally combine the ideas from the Patch chugin and @AndrewAday's tweens and signals proposal.

The Patch UGen

Currently the Patch UGen is a little hacky and takes in a function arg as a string and does member func lookup in-chugin due to the lack of function pointers in chuck currently. But ideally, a Patch UGen will eventually look like this:

SinOsc sin => Pan2 pan => dac;

// Our LFO is fed into Patch's input...
SinOsc lfo => Patch p => blackhole;
1 => lfo.freq;

// ... where it then updates the pan position at every tick
p.connect(pan.pan);

1::eon => now;

This is great! And it works with any member function that is

While this isn't completely general, I think it's good enough until chuck has a more robust type system.

However, this still feels kind of annoying to use (you have to remember to feed stuff into blackhole, p.connect has to be it's own separate line) and it doesn't really feel in-line with how chuck handles ugens in general. However, if the Patch ugen existed in this ideal form there's nothing stopping us from using some syntactic sugar to make this a much more intuitive experience.

The Syntactic Sugar

So in order to get this:

SinOsc s => dac;
SinOsc lfo => s.phase;

we basically need to have a translation step in the compiler to turn it into:

SinOsc s => dac;
SinOsc lfo => Patch lfo_patch => blackhole;
lfo_patch.connect(s.phase);

Doesn't seem too hard! The main hangup I see is when someone disconnects the ugen

lfo =< s.phase

This would have to somehow become

lfo =< lfo_patch;

Performance Questions

One concern is that not all parameters should be updated at signal rate, or it's recommended against because it's worse performance. In the simple case where it's literally just setting a variable the performance loss really isn't that bad and I don't think would be a bottleneck in most situations. However, when it's needed the control rate could be set like this:

SinOsc s => dac;
SinOsc lfo.rate(10::ms) => s.phase; // this would require constructors I think

Where rate sets how frequently the Patch ugen calls the update function

One weird quirk would be that if you tried to set lfo.rate later in the program, how would chuck know that it's specifically pointing to s.phase

Edge Cases

SinOsc lfo => s.phase => s.freq

this should be allowed, but

SinOsc lfo => s.phase => NRev rev;

Probably shouldn't

Feature Dependencies

nshaheed commented 1 year ago

@spencersalazar's suggestions:

yeah I guess one fully shaven yak version of this is 
support for variable rate processing graphs (e.g. SinOsc lfo.rate(10::hz) => (scaling, etc) => ug.param;
support for arbitrary math in ugen chains (SinOsc s / 2 => dac;) or something
IMO its super useful to just have arbitrary math/expressions/functions instead of a bunch of one-off ugens for scaling/etc. because then you can easily do like dB, exponential, and various other transforms easily
also unlocks fun stuff like exponential FM

Could potentially do this via ugen syntactic sugar or via chugens

nshaheed commented 1 year ago

More ideas...

@spencersalazar

IMO arbitrary expressions + multi-rate => params is a game changer
probably 75% of the chuck Ive ever written I could just delete haha 

Me:

imagine having an envelope follower ugen and just hooking it up like
 adc => EnvFollower e => sin.gain; 

@spencersalazar

like 🔥 🔥 🔥

SinOsc mod => SinOsc car => dac;
2 => car.op;
Gain g.rate(10::hz) * hid.mouseX * 100 => mod.freq;
Gain g.rate(10::hz) * hid.mouseY * 100 => mod.gain;

the Gain is kinda unsightly but could have some other syntax for stuff to enter into the signal chain domain

Me:

why not just let there be constants?
0.5.rate(10::hz) => osc.gain;

(aka make everything an object)