Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FM7 synth #12

Closed
gexahedron opened this issue Feb 23, 2022 · 16 comments
Closed

FM7 synth #12

gexahedron opened this issue Feb 23, 2022 · 16 comments

Comments

@gexahedron
Copy link

gexahedron commented Feb 23, 2022

Hi! Is it possible to add FM7 synth? I am especially interested in its arAlgo function. Thanks.

Also, I tried writing something like:

from sc3.synth import ugen as ugn

class FM7(ugn.MultiOutUGen):
    @classmethod
    def ar(cls, ctlMatrix, modMatrix):
        return cls._multi_new("audio", ctlMatrix, modMatrix)

    def _init_ugen(self, *inputs):
        self._inputs = inputs
        return self._init_outputs(6, self.rate)

but it will return "ChannelList([ChannelList([ChannelList([..." which I don't know how to convert into sound.

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

Hi, I'm looking at it, is it a quark or sc3-plugin? There is a way to add ugens from an external script.

@gexahedron
Copy link
Author

It's in sc3-plugins
https://github.com/supercollider/sc3-plugins/blob/main/source/SkUGens/sc/FM7.sc

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

The basic structure to make ugens available in an external module to the library is as follows:

from sc3.synth.ugen import MultiOutUGen
from sc3.synth.ugens import install_ugens_module

__all__ = ['FM7']

class FM7(MultiOutUGen):
    ...

install_ugens_module(__name__)

That will, at import time, add the classes defined in __all__ to the library's dictionary of ugens (used by SynthDesc and to write scsynth files). I've imported only the needed MultiOutUGen and install_ugens_module. Note that imports should be absolute. In sc3.synth.ugens.__init__.py are other functions to install packages or single ugens (adds just the class). To use it the following code should be enough:

from sc3.all import *
from mymodule import FM7

The library is first imported from sc3.all so it call the initialization logic first. This ways shouldn't be problems with cyclic imports or alike and the lib needs to be initialized first because it has a few global states. I was thinking in a way to make extra packages to be installed as namespace packages but haven't solve it yet, I'm not sure if it's possible to install an extra package to the library from the extra package itself.

ChannelList is an array of ugens that knows about ugen methods (through AbstractObject interface). It is returning nested ChannelLists because is doing multi channel expansion. Multi channel expansion happens inside _multi_new which calls _new1 which returns the return value of _init_ugen. That's the way it is in sclang, it could be simpler but I didn't dare to refactor that.

The way to avoid multi channel expansion if by using a tuple instead of a list. It acts the same way as a reference in sclang or nested arrays in some cases. But for this case you have to consider processing the constructor's arguments. The following would be the structure to what is needed to make it work, the slices part is missing, I don't know why slice returns nested lists of lists, since ChannelList is a list it could work but wouldn't be the best solution for Python.

import sc3.base.utils as utl
from sc3.synth.ugen import MultiOutUGen, ChannelList
from sc3.synth.ugens import install_ugens_module

# utl contains sclang list operations used by the library.
# 'utl' is the module alias used throught the library, I always use the same for each module, but you don't need to.


__all__ = ['FM7']


class FM7(MultiOutUGen):
    _control_matrix = ...
    _modulation_matrix = ...

    @classmethod
    def ar(cls, ctlmatrix=None, modmatrix=None):
        ctlmatrix = utl.flatten(ctlmatrix or cls._control_matrix, 1)
        modmatrix = utl.flatten(modmatrix or cls._modulation_matrix, 1)
        return cls._multi_new('audio', *ctlmatrix, *modmatrix)

    @classmethod
    def ar_algo(cls, algo=0, ctlmatrix=None, feedback=0.0):
        modmatrix, channels = cls._algo_spec(algo, feedback)
        return cls._slice(cls.ar(ctlmatrix, modmatrix), channels)

    @staticmethod
    def _slice(clst, channels):
        # slice is missing, it requires some specific logic.
        # clst is a ChannelList which is a python list.
        # clst = list(clst)  # returns a common python list, but it needs to be recursive...
        #                    # ChannelList shouldn't return nested ChannelList objects,
        #                    # but I don't know why is done like that.
        # ...
        # return ChannelList(clist)  # return the resulting python list as ChannelList.
        ...

    @classmethod
    def _algo_spec(cls, algo, feedback):
        ...  # return a tuple with the matrix as list of lists and channels as list.


install_ugens_module(__name__)

Please tell me if this doesn't make it more clear. I didn't test the last code.

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

I have some typos in the last part, sorry about that.

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

Tough class, code above is missing these methods:

    def _init_ugen(self, *inputs):  # override
        self._inputs = inputs
        return self._init_outputs(type(self)._num_required_inputs, self.rate)

    def _check_inputs(self):  # override
        if len(self._inputs) != type(self)._num_required_inputs:
            return 'some error message'

That's the way I would do it following the style in the library, it can be done in different ways.

@smrg-lm smrg-lm closed this as completed Feb 23, 2022
@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

Short answer: ChannelList is a python list and you can internally use it like that and/or create more instances as needed, but it has to be the return value of the ugen's constructor for multi channel expanded ugens otherwise synthdef function arithmetic will fail.

@gexahedron
Copy link
Author

gexahedron commented Feb 23, 2022

Thanks for such verbose answer!
I tried you code, unfortunately I'm not entirely versatile in the way the synths are written in your library (or in SuperCollider).
I copied it as is into my code, at first I got a problem that _num_required_inputs is undefined; then I wrote it into the class variables, tried various values, got the "some error message". Then the value 1 worked, but I got the following error:

TypeError                                 Traceback (most recent call last)
<ipython-input-10-4b3ca8361537> in <module>
     15 
     16 @synthdef
---> 17 def fm7(out=0, amp=1, spread=0.25, pan=0):
     18     sig = FM7.ar(ctlMatrix, modMatrix)
     19     sig = Splay.ar(sig, spread=spread, center=pan)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in synthdef(func, **kwargs)
   1029         return lambda func: _create_synthdef(func, **kwargs)
   1030     else:
-> 1031         return _create_synthdef(func)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _create_synthdef(func, **kwargs)
    977 
    978 def _create_synthdef(func, **kwargs):
--> 979     sdef = SynthDef(func.__name__, func, **kwargs)
    980     sdef.add()  # Running servers or offline patterns.
    981     sac.ServerBoot.add('all', lambda server: sdef.add())  # Next boot.

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in __init__(self, name, func, rates, prepend, variants, metadata)
    147         # self._callable_args = None
    148 
--> 149         self._build(func, rates or [], prepend or [])
    150 
    151     def _build(self, func, rates, prepend):

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _build(self, func, rates, prepend)
    154                 _libsc3.main._current_synthdef = self
    155                 self._init_build()
--> 156                 self._build_ugen_graph(func, rates, prepend)
    157                 self._finish_build()
    158                 self._func = func

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _build_ugen_graph(self, func, rates, prepend)
    214         prepend = utl.as_list(prepend)
    215         self._args_to_controls(func, rates, len(prepend))
--> 216         result = func(*(prepend + self._build_controls()))
    217         self._control_names = save_ctl_names
    218         return result

<ipython-input-10-4b3ca8361537> in fm7(out, amp, spread, pan)
     17 def fm7(out=0, amp=1, spread=0.25, pan=0):
     18     sig = FM7.ar(ctlMatrix, modMatrix)
---> 19     sig = Splay.ar(sig, spread=spread, center=pan)
     20     sig = sig * amp
     21     Out.ar(out, sig)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/ugens/pan.py in ar(cls, inputs, spread, level, center, level_comp)
    241     @classmethod
    242     def ar(cls, inputs, spread=1, level=1, center=0.0, level_comp=True):
--> 243         return cls._multi_new(
    244             'audio', spread, level, center, level_comp, *inputs)
    245 

TypeError: _multi_new() argument after * must be an iterable, not OutputProxy

Or is it related to that I tried to use Splay.ar on it?

@gexahedron
Copy link
Author

gexahedron commented Feb 23, 2022

As for

    def _algo_spec(cls, algo, feedback):
        ...  # return a tuple with the matrix as list of lists and channels as list.

Do I need to copy the matrices from FM7.sc SuperCollider class code?

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

Yes, matrices are the same but they are within lambdas, requires some boilerplate. The error is because the missing parts (multi out ugens return output proxies within channel list, that looks weird but is normal). I didn't define everything because don't have the time now, is not much what is missing but I need some time to check the rest.

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 23, 2022

I think this should work, I didn't transcribed the algorithm data except for the first one.

from copy import deepcopy

import sc3.base.utils as utl
from sc3.synth.ugen import MultiOutUGen, ChannelList
from sc3.synth.ugens import install_ugens_module


__all__ = ['FM7']


NUM_OP = 6
NUM_CTL = 3
ALGOS = [
    [
        [  # x, y, value
            [0, 1, 1],
            [2, 3, 1],
            [3, 4, 1],
            [4, 5, 1],
            [5, 5, None]
        ],
        [4, 2],  # feedback parameter position (filled with None above)
        [0, 2]   # output channel numbers
    ],
    # ... TODO: All other algorithm specs.
]

class FM7(MultiOutUGen):
    _required_inputs = NUM_OP * NUM_CTL + NUM_OP * NUM_OP
    _default_ctlmatrix = [[0 for _ in range(NUM_CTL)] for _ in range(NUM_OP)]
    _default_modmatrix = [[0 for _ in range(NUM_OP)] for _ in range(NUM_OP)]

    @classmethod
    def ar(cls, ctlmatrix=None, modmatrix=None):
        ctlmatrix = utl.flatten(ctlmatrix or cls._default_ctlmatrix, 1)
        modmatrix = utl.flatten(modmatrix or cls._default_modmatrix, 1)
        return cls._multi_new('audio', *ctlmatrix, *modmatrix)

    @classmethod
    def ar_algo(cls, algo=0, ctlmatrix=None, feedback=0.0):
        modmatrix, channels = cls._algo_spec(algo, feedback)
        output = cls.ar(ctlmatrix, modmatrix)
        return ChannelList([output[c] for c in channels])

    @classmethod
    def _modulation_matrix(cls, *args):
        m = cls._default_modmatrix
        for x, y, value in args:
            m[x][y] = value
        return m

    @classmethod
    def _algo_spec(cls, num, feedback):
        spec, (i, j), channels = ALGOS[num]
        spec = deepcopy(spec)
        spec[i][j] = feedback
        return cls._modulation_matrix(*spec), channels

    def _init_ugen(self, *inputs):  # override
        self._inputs = inputs
        return self._init_outputs(type(self)._required_inputs, self.rate)

    def _check_inputs(self):  # override
        if len(self._inputs) != type(self)._required_inputs:
            return (
                f'{type(self)._required_inputs} inputs required, '
                f'{len(self._inputs)} received')


install_ugens_module(__name__)

@gexahedron
Copy link
Author

Thanks!

I tried to use your code like this:

ctlMatrix = [ [ 300, 0,    1   ],
  [ 400, math.pi/2, 1   ],
  [ 730, 0,    0.5 ],
  [ 0,   0,    0   ],
  [ 0,   0,    0   ],
  [ 0,   0,    0   ] ];
modMatrix = [
        [0.001, 0.1, 0, 1, 0, 0],
        [1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0]
    ];

@synthdef
def fm7(ctlMatrix, modMatrix, out=0, amp=1, spread=0.25, pan=0):
    sig = FM7.ar(ctlMatrix, modMatrix)
#     sig = FM7.ar_algo(0, ctlMatrix)
#     sig = Splay.ar(sig, spread=spread, center=pan)
#     sig = sig * amp
    Out.ar(out, sig)


fm7(ctlMatrix, modMatrix)

and get an error like this:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-326c6a1a1bed> in <module>
     15 
     16 @synthdef
---> 17 def fm7(ctlMatrix, modMatrix, out=0, amp=1, spread=0.25, pan=0):
     18     sig = FM7.ar(ctlMatrix, modMatrix)
     19 #     sig = FM7.ar_algo(0, ctlMatrix)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in synthdef(func, **kwargs)
   1029         return lambda func: _create_synthdef(func, **kwargs)
   1030     else:
-> 1031         return _create_synthdef(func)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _create_synthdef(func, **kwargs)
    977 
    978 def _create_synthdef(func, **kwargs):
--> 979     sdef = SynthDef(func.__name__, func, **kwargs)
    980     sdef.add()  # Running servers or offline patterns.
    981     sac.ServerBoot.add('all', lambda server: sdef.add())  # Next boot.

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in __init__(self, name, func, rates, prepend, variants, metadata)
    147         # self._callable_args = None
    148 
--> 149         self._build(func, rates or [], prepend or [])
    150 
    151     def _build(self, func, rates, prepend):

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _build(self, func, rates, prepend)
    154                 _libsc3.main._current_synthdef = self
    155                 self._init_build()
--> 156                 self._build_ugen_graph(func, rates, prepend)
    157                 self._finish_build()
    158                 self._func = func

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/synth/synthdef.py in _build_ugen_graph(self, func, rates, prepend)
    214         prepend = utl.as_list(prepend)
    215         self._args_to_controls(func, rates, len(prepend))
--> 216         result = func(*(prepend + self._build_controls()))
    217         self._control_names = save_ctl_names
    218         return result

<ipython-input-11-326c6a1a1bed> in fm7(ctlMatrix, modMatrix, out, amp, spread, pan)
     16 @synthdef
     17 def fm7(ctlMatrix, modMatrix, out=0, amp=1, spread=0.25, pan=0):
---> 18     sig = FM7.ar(ctlMatrix, modMatrix)
     19 #     sig = FM7.ar_algo(0, ctlMatrix)
     20 #     sig = Splay.ar(sig, spread=spread, center=pan)

<ipython-input-2-0f80586deaac> in ar(cls, ctlmatrix, modmatrix)
     79     @classmethod
     80     def ar(cls, ctlmatrix=None, modmatrix=None):
---> 81         ctlmatrix = utl.flatten(ctlmatrix or cls._default_ctlmatrix, 1)
     82         modmatrix = utl.flatten(modmatrix or cls._default_modmatrix, 1)
     83         return cls._multi_new('audio', *ctlmatrix, *modmatrix)

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/base/utils.py in flatten(inlist, n_levels)
     54                 outlist.append(item)
     55     outlist = []
---> 56     _(inlist, outlist, 0)
     57     return outlist
     58 

~/miniconda3/envs/py38/lib/python3.8/site-packages/sc3/base/utils.py in _(inlist, outlist, n)
     45 def flatten(inlist, n_levels=1):
     46     def _(inlist, outlist, n):
---> 47         for item in inlist:
     48             if n < n_levels:
     49                 if isinstance(item, list):

TypeError: 'OutputProxy' object is not iterable

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 24, 2022

That is because the synthdef function argument is shadowing the global variables of the same name.

PD: These questions are ok and welcome, it would be better next time if instead of creating an issue you create a discussion on the other github tab.

@gexahedron
Copy link
Author

gexahedron commented Feb 24, 2022

Thanks!
Sorry, didn't know about discussions tab : )

I've renamed the synthdef function and it didn't help, got the same error

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 24, 2022

No problem. I was referring to the variable names:

myvar = 123

def func(myvar=321):
    print(myvar)

func()

That will prints 321 because is the value of the argument, myvar declared as a parameter of func creates a local variable that overrides myvar defined at the beginning of the snippet. A function decorated with @synthdef will replace the arguments with control ugen objects so they can interact with the other ugen objects within that function as a synthesis graph.

@gexahedron
Copy link
Author

gexahedron commented Feb 28, 2022

Oh, interesting, thanks! It works now : )
One more (maybe last) question - so, it's not possible to dynamically change ctlmatrix and modmatrix with function parameters? Only with something like things like Line etc.?
And I guess it's the same as in SuperCollider's FM7, right?

@smrg-lm
Copy link
Owner

smrg-lm commented Feb 28, 2022

It should be exactly the same same as with sclang code. If the fields of the matrix are controls you can externally map values to the synth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants