musikinformatik / that

Real time audio analysis library
GNU General Public License v3.0
9 stars 2 forks source link

Class design #1

Closed capital-G closed 2 years ago

capital-G commented 3 years ago

I started today to transform the code into OO code and here is a first draft of the interface

s.boot;

// create signal
a = Ndef(\foo, {SinOsc.ar(200)*0.5});

// create some analyzers
b = ThatFreqAnalyzer(\freqFoo, callback: {});
c = ThatAmpAnalyzer(\ampFoo, callback: {|amp| amp;});

// add analyzers to input to account 1:n realtionship
ThatInput(\abc, a).add(b, c)

// an analyzer can also be cleared
c.clear;

// each analyzer can now be modified and the latest value can
// be accesed via
b.v;

// or attached to a new source
d = ThatInput(\bar, Ndef(\aoeu, {SinOsc.ar(400)*0.1})).add(b);

b.v
// returns now 400

// a minimal and quick example looks like this
Ndef(\baz, {SinOsc.ar(LFNoise2.kr(10, SinOsc.kr(0.02, mul: 1000), 1000), SinOsc.kr(0.2))*0.1}).play;

ThatInput(\bazIn, Ndef(\baz)).add(ThatFreqAnalyzer(\foo3, {|f| f.postln;}));

// via the base class ThatAnalyzer it is possible to create dynamically new analyzers in the interpreter runtime

Questions

maybe a first PR will be submitted during this weekend

telephon commented 3 years ago
s.boot;

// create signal
a = Ndef(\foo, {SinOsc.ar(200)*0.5});

=> general comment:normally, you don't have to double-keep the Ndef, you can just reference it by Ndef(\foo)

// create some analyzers
b = ThatFreqAnalyzer(\freqFoo, callback: {});
c = ThatAmpAnalyzer(\ampFoo, callback: {|amp| amp;});

ok.

// add analyzers to input to account 1:n realtionship
ThatInput(\abc, a).add(b, c)
// an analyzer can also be cleared
c.clear;

If you want it to work like Ndefs you should be able to clear it like this:

ThatAmpAnalyzer(\ampFoo).clear
// each analyzer can now be modified and the latest value can
// be accesed via

b.v;

This saves typing but is hard to explain and a bit unidiomatic. Better you full names (think "Smalltalk"), like prevValue, or prevAmp.

// or attached to a new source
d = ThatInput(\bar, Ndef(\aoeu, {SinOsc.ar(400)*0.1})).add(b);

b.v
// returns now 400
// a minimal and quick example looks like this
Ndef(\baz, {SinOsc.ar(LFNoise2.kr(10, SinOsc.kr(0.02, mul: 1000), 1000), SinOsc.kr(0.2))*0.1}).play;

ThatInput(\bazIn, Ndef(\baz)).add(ThatFreqAnalyzer(\foo3, {|f| f.postln;}));

// via the base class ThatAnalyzer it is possible to create dynamically new analyzers in the interpreter runtime

I don't understand yet what ThatInput will do, but I can guess that it collects analyzers? But why do you have to do that?

In general, this is a good direction.

telephon commented 3 years ago

P.S.

In general, every class that works according to the def schema (like Ndef, OSCdef etc.) has an anonymous base class that can be instantiated without a name (like NodeProxy, OSCFunc etc.).

I tend to use the syllable def in classes that work like this.

capital-G commented 3 years ago
// add analyzers to input to account 1:n realtionship
ThatInput(\abc, a).add(b, c)
  • I don't understand this "1:n relationship"?

    • can you also add signals that are not Ndefs?

well, you have 1 signal and up to n analyzers on one signal - much like a proxychain. the motivation was the intuition of calling Ndef(\foo).analyzers.amp, which could alternatively implemented via Analyzer(Ndef(\foo)).amp. But at the same time we do not want to limit ourselves to Ndefs, so I created this. I am not convinced if this was a good Idea, I need to sleep over it.

// an analyzer can also be cleared
c.clear;

If you want it to work like Ndefs you should be able to clear it like this:

ThatAmpAnalyzer(\ampFoo).clear

It is indeed implemented in such a way.

// each analyzer can now be modified and the latest value can
// be accesed via

b.v;

This saves typing but is hard to explain and a bit unidiomatic. Better you full names (think "Smalltalk"), like prevValue, or prevAmp.

I am also not convinced by v - the obvious value should probably not overwritten here and previousValue seemed a bit long for SC standards, but I also prefer longer and more precise names. Another name for this that was suggested to me was simply .get.

I don't understand yet what ThatInput will do, but I can guess that it collects analyzers? But why do you have to do that?

Yeah, I think it is unnecessary. It is nice if you want to run multiple analyzers on one source and move them all to another source. But this principle contradicts with the ownership of who changes the input, see https://github.com/musikinformatik/that/blob/a1f3f3e46de4ff153c8ff28ead4ef3a4e2f4ac90/classes/that.sc#L38-L42

Regarding the Def problematic: Yeah, I am aware of this, probably there should be a non-def version of it as well. Does sclang offer something like Python MixIns? This would creating such things like ...def really easy. And btw - for nostalgia sake: Why are those called def? The functionality mimics more a dict or a cache and not a "def" (which stands for definition?) ?

Will continue with dev tomorrow.

telephon commented 3 years ago

well, you have 1 signal and up to n analyzers on one signal - much like a proxychain.

you don't need this because you can route the signals. For this, I wouldn't follow the example of ProxyChain. That is only useful when order is important, and here it isn't.

But at the same time we do not want to limit ourselves to Ndefs, so I created this.

This is why I used a function as an input. If you want more signals, just let the function return the sum of them (e.g. { Ndef.ar(\x) + Ndef.ar(\y) }, but also just { SoundIn.ar(0) } if you don't work with node proxies).

Another name for this that was suggested to me was simply .get.

You could also write value. In general, you may want to return an event.

Does sclang offer something like Python MixIns?

No. But for this, subclassing is fine – you can check out how Pdefn is implemented, I think that is the simplest case.

capital-G commented 3 years ago

https://github.com/musikinformatik/that/pull/2/commits/57908cab23f3f0933f09fc0910794a1d57d23f1b removed ThatInput.

Maybe it is ok to solely provide the def version as we need an unique identifier for the OSC communication channels? one could generate UUIDs under the hood for this but I am not a fan of such implicitness.

On another note: https://github.com/musikinformatik/that/blob/743d7ec6142634a1d847f1c95349835bab916e77/library/that-system-functions.scd#L55

Is there a reason that this is only done on global triggers and not everytime? Having access to an array of all values makes more sense to me than to filter out all events manually.

I am also confused what the best way would be to handle triggers

Providing a lambda function could maybe allow such things, python example ahead

def amp_analysis(name: str, input: UGen, callback: Callable, trigger: Callable=None):
  trigger = trigger(input, StanardTrigger()) if trigger else StandardTrigger()
  pass

def my_callback(amp):
  print(amp)

amp_analysis(
  name="foo",
  input=SinOsc.ar(200),
  callback=my_callback,
  trigger=lambda input, internalTrigger: input>0.5 or internalTrigger
)
capital-G commented 3 years ago

https://github.com/musikinformatik/that/pull/2/commits/851c9fd1c8e2dcd97d1dafe6a36d3da644fcdda7 implemented lambda triggers which now can be used like

ThatAmp(
    name: \aAmp,
    input: Ndef(\a, {
        SinOsc.ar(200)*EnvGen.kr(Env.perc, gate: Impulse.kr(0.2))
    }),
    callback: {|a| a.postln},
    trigger: {|in, trig| (trig+Impulse.kr(5.0))>=1} // arithmetic or
)
capital-G commented 3 years ago

One thing that came to my mind and I do not know what the best way to implement is:

q = ();

// create a ampanalyzer
q[\amp] = ThatAmp(
    name: \a,
    input: Ndef(\a, {
        SinOsc.ar(200)*EnvGen.kr(Env.perc, gate: Impulse.kr([1.2, 1.0]))
    }),
    callback: {|a| a.postln},
    trigger: {|in, trig| Impulse.kr(1.0)}
);

// now lets create a freq analzyer as well
q[\freq] = ThatFreq(
    name: \a,
    input: Ndef(\a),
    callback: {|a| a.postln},
    trigger: {|in, trig| Impulse.kr(1.0)}
);

this now clashes/fails as ThatFreq and ThatAmp use the name a but share the same That namespace which is not obvious!

A benefit of using a common namespace in That is that we can access all available analyzers via the base class That. We could still make this work by prepending e.g. an amp_ on the name of an AmpAnalyzer to fake a separated namespace - but this is not obvious when one wants to access this from That, see


ThatAmp(\a, Ndef(\foo), {|a| a.postln});

// does not work
That(\a)

// does work but not obvious
That(\amp_a)

Another solution would be to create a static function for each analyzer that serves as a contstructor. this makes it more obvious that naming analyzers share the same namespace of That.

a = That.amp(
    name: \a,
    input: Ndef(\a, {
        SinOsc.ar(200)*EnvGen.kr(Env.perc, gate: Impulse.kr([1.2, 1.0]))
    }),
    callback: {|a| a.postln},
    trigger: {|in, trig| Impulse.kr(1.0)}
)

That(\a).latestValue

but how to access/modify parameters specific to an amp analyzer?

edit: on another note: lets say the def interface of That allows to replace the analyzer while running, so

ThatAmp(\foo, input: Ndef(\foo), callback: {|a| a.postln;});

That(\amp_foo).analyzer = // set to e.g. ThatFreq somehow or something custom

Now the namespace is completely messed up.

capital-G commented 3 years ago

I gone now for the constructor approach as the parameters of each analyzer function can be accessed from the Node and do not need to be managed by the class.

Currently it looks like this

That.freq(
    name: \aFoo,
    input: Ndef(\a, {SinOsc.ar(200) * SinOsc.ar(0.1)}),
    callback: {|a| a[0].postln},
    trigger: {Impulse.kr(1.1)}
);

That.all;
// returs -> ( 'aFoo': a That )

// edit analyzer specific arguments as ndef, e.g.
That(\aFoo).analyzer.gui;
capital-G commented 3 years ago

Except for the missing tests and not finished documentation I think the quark in #2 is in a finished state.

capital-G commented 2 years ago

2 is merged so this can be closed.