Filter Messages & Implicit Rates

The file FilterMethods.sc below has message definitions for various filter UGens.

It can make writing signals in left to right form a bit simpler in some cases, i.e.

Dust.ar(1).mul(0.25).decay(0.2).mul(WhiteNoise.ar()).allpassN(0.2, 0.2, 3)

c.f. the same graph written using prefix filters:

AllpassN.ar(Decay.ar(Dust.ar(1) * 0.25, 0.2) * WhiteNoise.ar, 0.2, 0.2, 3)

https://gitlab.com/rd--/stsc3/-/blob/master/sc/FilterMethods.sc

The file ImplicitRateConstructors.sc has *new definitions to allow implicit rates for some oscillator UGens.

The same graph again, eliding .ar, i.e.

Dust(1).mul(0.25).decay(0.2).mul(WhiteNoise()).allpassN(0.2, 0.2, 3)

https://gitlab.com/rd--/stsc3/-/blob/master/sc/ImplicitRateConstructors.sc

Ps. The files above are auto-generated. Iā€™ve only made variants for some UGens, but you can make them all if you like, c.f.

https://gitlab.com/rd--/hsc3-db/-/blob/master/Sound/SC3/UGen/DB/Bindings/SuperCollider.hs

5 Likes

This is great! One minor snag: the example doesnā€™t work for me because the mul method is not defined, so I assume you have defined it in some other extension. I have added this to both the + UGen and + Array extensions:

mul {
    arg mul;
    ^MulAdd(this, mul);
}

And now it works great! I am always looking for ways to reduce keystrokes, and I love the method-chaining style as well, so I am really digging this. By the way, your terse guide is awesome too. You are making some excellent contributions!

thanks for this!

I use the following for a similar purpose:

+ Object{
	=> {|a| ^a.(this)}
}

so I can write

{ Impulse.ar(5) => FreeVerb.ar(_,1,1)  * 0.1 => LPF.ar(_,800,2) }.play

the one annoyance is that it breaks method chaining ā€¦

Youā€™re correct, apologies, mulā€™s in a different file!

Unfortunately add doesnā€™t work (Sc Arrays arenā€™t fixed size and redefining add causes havoc), but mulAdd doesā€¦

[1, 2, 3].mulAdd(4, 5) == [9, 13, 17]

Aha, => is very elegant!

Iā€™m curious about the particular dot notation above because it works equally well for Scheme(s).

If we translate:

Dust(1).mul(0.25).decay(0.2).mul(WhiteNoise()).allpassN(0.2, 0.2, 3)

to Scheme as:

(allpassn (mul (decay (mul (dust 1) 0.25) 0.2) (whitenoise)) 0.2 0.2 3)

it plays!

There is a translator below, in case anyoneā€™s curious.

https://gitlab.com/rd--/hsc3-lisp/-/blob/master/Sound/SC3/Lisp/SuperCollider.hs

f(x) => (f x) and x.f => (f x) and x.f(y) => (f x y) and f() => (f) and f(x).g(y) => (g (f x) y) &etc.

Ps. Below is a tiny demonstration video of sending the same notation to 1. SuperCollider, 2. Smalltalk and 3. Scheme.

Itā€™s a bit obscure, but the Emacs ā€œmode-lineā€ shows which mode itā€™s in, and for the ā€œtranslationsā€ the translated text is displayed in the ā€œecho areaā€.

https://vimeo.com/640256669

Not sure exactly what the point of this is, perhaps just that for simple domains Scheme is a very simple and lovely language!

just a note that I would favor the implicit rate constructor being part of SC - the mental overhead of selecting a rate when instantiating every Ugen is annoying (and 3 extra characters!)

| semiquaver
October 29 |

  • | - |

just a note that I would favor the implicit rate constructor being part of SC - the mental overhead of selecting a rate when instantiating every Ugen is annoying (and 3 extra characters!)

It could easily be done by reserving the new constructor for UGens and properly implementing ir, kr, ar, dr constructors but It will break a lot of code. I also noted, personally, that writing the rate makes it easy to understand the graph.

I guess the idea would be to add the implicit rate constructors as an option, but not a requirement.

Currently UGens do not use *new at all:

Meta_UGen.findRespondingMethodFor(\new)
-> Meta_Object:new

ā€¦ so implementing a *new constructor wouldnā€™t break anything that exists in the vanilla main library.

I suppose here we run up against the typical SC-land trade-off: between expressiveness and consistency.

I wonā€™t deny that this is a nice way to write unidirectional signal chains.

At the same time, itā€™s been raised before that SC is challenging to learn because there are ā€œtoo many ways to do the same thing.ā€ One of the ways that we end up with ā€œtoo many waysā€ is by adding conveniences without first considering potential drawbacks (and once itā€™s in, deprecation becomes a big deal).

To be honest, for myself, neither of these two is especially readable:

Dust.ar(1).mul(0.25).decay(0.2).mul(WhiteNoise.ar()).allpassN(0.2, 0.2, 3)

AllpassN.ar(Decay.ar(Dust.ar(1) * 0.25, 0.2) * WhiteNoise.ar, 0.2, 0.2, 3)

I tend to write:

var sig = Dust.ar(1) * 0.25;
sig = Decay.ar(sig, 0.2) * WhiteNoise.ar;
sig = AllpassN.ar(sig, 0.2, 0.2, 3);

Maximally compact? No. But (this is just a personal opinion) ā€“ 1/ Capitalizing UGen names distinguishes them from inputs and draws attention to them in a way that I donā€™t get from the left-to-right filter methods. (Also, class names are highlighted while method names arenā€™t.) 2/ The visual separation between stages is (somehow?) useful or comforting to me.

WRT 2/, we could try:

Dust.ar(1)
.mul(0.25)
.decay(0.2)
.mul(WhiteNoise.ar())
.allpassN(0.2, 0.2, 3)

At least this way, you have a consistent place to look for the operation being performed, instead of having to scan a long line without any visual clues about what is adding UGens and what is just (say) a variable reference.

hjh

1 Like

Iā€™d probably write

Dust.ar(1) * 0.25 => Decay.ar( _, 0.2)
* WhiteNoise.ar() 
=> AllPassN.ar( _, 0.2, 0.2, 3)

to my eye the variable name ā€˜sigā€™ (five occurances!) is noise + finding it after the equals take a moment to boot. If I need it for further calculation I can assign it at the top of the chain.

re the .new method I think it should default to ar. To my mind the control rate is becoming a relic anyway, most useful as a ā€œspecial caseā€ for low resource platforms and in a more modern setup the more an opportunity for errors than anything else. I still use it out of old habits but Iā€™d love to forget about it!

Suppose the decay time is variable (signal input rather than a constant). The only way to pass that will be as a variable, because _ becomes thorny as soon as thereā€™s more than one method call ā€“ Iā€™m not sure what would happen with e.g. => Decay.ar(_, SinOsc.kr(0.1, 0, 0.2, 0.25)).

Also, expressive variable names are a form of self-documentation, which you might think is unimportant until you try to update a four-year-old SynthDef and you find that you have no idea ā€œwhat the @@@@ was I thinking???ā€

hjh

happily => Decay.ar(_, SinOsc.kr(0.1, 0, 0.2, 0.25))

does work ok

what doesnā€™t work, as you intuited, is
Dust.ar(1) => Decay.ar(_, 0.2).distort

there I have to write:
Dust.ar(1) => Decay.ar(_,0.2) => _.distort
or
Dust.ar(1) => {|i| Decay.ar(i, 0.2).distort}
which last is not so beautiful.

Agree re: names, especially when there are mutiple limbs in the tree - but I still prefer to see fewer of them!:

var sig = Dust.ar(1) * 0.25 
  => Decay.ar( _, 0.2) * WhiteNoise.ar() 
  => AllPassN.ar( _, 0.2, 0.2, 3);
var cutoff = EnvGen .....

OK, thatā€™s good. One reason why I use _ in simple cases only is that itā€™s not clear where the boundary is.

Which isā€¦ often. Very often.

hjh

This is a bit off topic, but I have been wondering about that as well. Even James McCartney said in his lecture ā€œSuperCollider and Timeā€ that he would probably get rid of control rate if he could.

I am currently in the process of designing an audio programming language/library (so far only on pen and paper :slight_smile:) and I have been thinking about control rate a lot. I would say that with a block size of 64 samples the CPU savings are certainly noticable but not spectacular. However, my audio engine would support subgraphs that can run at lower/higher block sizes. My main goals are local single sample feedback and FFT via reblocking (similar to Pd), but it would also allow you to run your control rate ugens in a dedicated subgraph at a large blocksize. With a local block size of 1024 samples and many ugens, the performance difference could be quite large.

On the language side, I would like the rates to be automatically deduced as often as possible. Also, I would prefer a more stream-like syntax for connecting ugens - a bit like yours! Therefore I find this thread quite interesting.

1 Like

Also, the notation isnā€™t particular to filters.

The file https://gitlab.com/rd--/stsc3/-/blob/master/sc/OscillatorMethods.sc has some ā€œoscillatorā€ methods, c.f.

MouseX(0.5, 20, 1, 0.2).impulse(0).sweep(700).add(500).sinOsc(0).mul(0.1)

Or longer:

0.4.lfSaw(0).mul(24).add([8, 7.23].lfSaw(0).mulAdd(3, 80)).midicps.sinOsc(0).mul(0.04).combN(0.2, 0.2, 4).mul(0.1)

Ps. A nice thing about systems where f(x,y)=x.f(y) is that you get both left to right and right to left ā€œfor freeā€, meaning that the variations:

mousex(0.5, 20, 1, 0.2).impulse(0).sweep(700).add(500).sinosc(0).mul(0.1)
mul(sinosc(add(sweep(impulse(mousex(0.5, 20, 1, 0.2), 0), 700), 500), 0), 0.1)

denote the same expression, thereā€™s no distinction, you donā€™t define names twice &etc.

Pps. In D they call this ā€œUniform Function Call Syntaxā€ https://tour.dlang.org/tour/en/gems/uniform-function-call-syntax-ufcs. Itā€™s an old idea!

Ppps. Thereā€™s also https://gitlab.com/rd--/stsc3/-/blob/master/sc/FilterConstructors.sc, which has some implicit rate filter constructors, used in the demonstration video earlier.

About control rate, I think itā€™s interesting that it can also fall out as a special case of ā€œdemand rateā€, simply by rewriting references to the sample rate in the demand subgraph.

Ie. if sr = 48000, control rate with a block size of 48, perhaps written kr(ā€¦), can be implemented as demand(impulse(1000), multiplySampleRateBy(1/1000, ā€¦)) &etc.

Writing Sc graphs without rate qualifiers can be helpful if you want to send the same notation to both scsynth and to a simple home made synthsesiser that doesnā€™t have an ar/kr distinction.

| jamshark70
October 31 |

  • | - |

lucas:

It could easily be done by reserving the new constructor for UGens and properly implementing ir, kr, ar, dr constructors but It will break a lot of code.

I guess the idea would be to add the implicit rate constructors as an option, but not a requirement.

Currently UGens do not use *new at all:

Meta_UGen.findRespondingMethodFor(\new)

-> Meta_Object:new

ā€¦ so implementing a *new constructor wouldnā€™t break anything that exists in the vanilla main library.

I was thinking about demand rate ugens and special cases that already use new as a lot of code in risk if it was going to be implemented from the top of the class hierarchy, but maybe there is a way to keep compatibility. I do agree with your later observations.

I am currently in the process of designing an audio programming language/library (so far only on pen and paper :slight_smile:) and I have been thinking about control rate a lot. I would say that with a block size of 64 samples the CPU savings are certainly noticable but not spectacular. However, my audio engine would support subgraphs that can run at lower/higher block sizes. My main goals are local single sample feedback and FFT via reblocking (similar to Pd), but it would also allow you to run your control rate ugens in a dedicated subgraph at a large blocksize. With a local block size of 1024 samples and many ugens, the performance difference could be quite large.

There is a nice language for low level faust like code generation that is multidimensional and multirate: https://github.com/jleben/arrp Iā€™m not sure if it manages data between different rate running apps or just inside the generators.

1 Like

As one (last!) aspect of this, the file https://gitlab.com/rd--/stsc3/-/blob/master/sc/RewriteRate.sc has a kr method that traverses the input UGen and lowers audio rate nodes to control rate. So that in:

Balance2(LFSaw(44, 0), Pulse(33, 0.5), SinOsc(Rand(0.25, 0.75), 0).kr, 0.1)

SinOsc is control rate, Rand is scalar rate, and the remainder are audio rate.