[1]:
import time

import sc3nb as scn
from sc3nb import SynthDef, Synth
[2]:
sc = scn.startup()
Starting sclang process... [sclang | start reading ]
Done.
Registering OSC /return callback in sclang... Done.
Loading default sc3nb SynthDefs... Done.
Booting SuperCollider Server... [scsynth | start reading ]
Done.

SynthDef

SynthDef wraps and extends the flexibility of SuperCollider Synth Definitions

To see how to create and use a Synth from the SynthDef please also see the Synth examples

SynthDef creation

[3]:
synth_def = SynthDef('random',
"""{ |out|
    var osc, env, freq;
    freq = Rand(400, 800);
    osc = SinOsc.ar(freq, 0, 0.2);
    env = Line.kr(1, 0, 1, doneAction: Done.freeSelf);
    Out.ar(out, osc * env);
}""")

Note that you can copy the representation to you SuperCollider IDE

[4]:
synth_def
[4]:
SynthDef('random', { |out|
    var osc, env, freq;
    freq = Rand(400, 800);
    osc = SinOsc.ar(freq, 0, 0.2);
    env = Line.kr(1, 0, 1, doneAction: Done.freeSelf);
    Out.ar(out, osc * env);
})

SynthDefs are created via sclang when you add them.

[5]:
synth_def.add()
[5]:
'random'
[6]:
Synth("random")
[6]:
<Synth(20001) 'random' s {}>

Loading SynthDefs

You can also load SynthDefs from .scsynthdef files or as bytes via the Server

The Server allows - to send binary SynthDef content with send_synthdef

[7]:
sc.server.send_synthdef?
  • load a SynthDef file with load_synthdef

  • load a directory with SynthDef files with load_directory

[8]:
sc.server.load_synthdef?

This can be used together with supriya for example.

Note: These synth are not known to sclang, therefore getting the SynthDesc won’t work

[9]:
try:
    from supriya.synthdefs import SynthDefBuilder, Envelope
    from supriya.ugens import SinOsc, EnvGen, Out
    from supriya import DoneAction
except:
    print("Example needs supriya pakage installed")
else:
    with SynthDefBuilder(name='supriya_synth', amplitude=0.3, frequency=440.0, gate=1.0) as builder:
        source = SinOsc.ar(frequency=builder['frequency'])
        envelope = EnvGen.kr(done_action=DoneAction.FREE_SYNTH,
                             envelope=Envelope.asr(),
                             gate=builder['gate'])
        source = source * builder['amplitude']
        source = source * envelope
        out = Out.ar(bus=0, source=source)

    # Build SynthDef with supriya SynthDefBuilder
    supriya_synthdef = builder.build()
    # Compile SynthDef to binary SynthDef and send it to the server.
    sc.server.send_synthdef(supriya_synthdef.compile())

    with sc.server.bundler(0.1) as bundler:
        syn1 = scn.Synth(supriya_synthdef.actual_name)  # the SynthDef name is saved in the supriya_synthdef
        bundler.wait(0.5)
        syn1.release(0.5)

        bundler.wait(1)

        syn2 = scn.Synth(supriya_synthdef.actual_name, controls={"frequency": 220})
        bundler.wait(1)
        syn2.free()
Example needs supriya pakage installed

SynthDef creation with context

the sc3nb SynthDef - different from standard sclang SynthDefs - allows to inject a number of context specifiers using mustache syntax, i.e. {{my_context_specifier}}. This would mark a place within the synth definition that can be replaced by some user-wished sclang code before adding the SynthDef to the server.

You can create a ‘proto’ synth definition with the SynthDef

[10]:
synth_def_context = SynthDef(name="myKlank", definition=r"""
{ |out=0, amp=0.3, freq=440|
    var klank = DynKlank.ar(`[[1,2], [1,1], [1.4,1]], {{EXCITER}}, freq);
    Out.ar(out, amp*klank!^p_channels);
}""")

In this example a new definition named "myKlank" offers a dynamic context {{EXCITER}}” and 3 value contexts freqs, rings, and channels. Now we want to replace the dynamic context EXCITER with user-specific code.

  • use set_context() to replace a context with specific code

[11]:
synth_def_context.set_context("EXCITER", "Dust.ar(20)")
[11]:
SynthDef('myKlank',
{ |out=0, amp=0.3, freq=440|
    var klank = DynKlank.ar(`[[1,2], [1,1], [1.4,1]], Dust.ar(20), freq);
    Out.ar(out, amp*klank!^p_channels);
})
  • the specific code can include other context specifier strings in turn

    • this basically allows to create chained processing in python-compiled synths defs.

Remarks: * The context mechanism is very general: * it can be used both for code and values * e.g. for an array specification or a UGen selection. * To set a value (e.g. number or array), consider to use the pyvars syntax, i.e. using the caret ‘^’ variable value injection of python variables into sc code

The definition is complete up to the pyvars. So let’s create a myKlank in SC!

[12]:
p_channels = 2
[13]:
synth_def_context  # will show ^p_channels value when defined
[13]:
SynthDef('myKlank',
{ |out=0, amp=0.3, freq=440|
    var klank = DynKlank.ar(`[[1,2], [1,1], [1.4,1]], Dust.ar(20), freq);
    Out.ar(out, amp*klank!2);
})
[14]:
synth_name = synth_def_context.add(name="kdust")
[15]:
# for testing, let's create a synth and stop after 1s
%scv x = Synth.new(^synth_name, [\freq, 200, \amp, 0.05])
time.sleep(1)
%scv x.free
-> Synth('kdust' : 1000)
-> Synth('kdust' : 1000)

Now let’s create another synthdef with a WhiteNoise excitation, but as a 1-channel version

[16]:
p_channels = 1
[17]:
synth_def_context.reset()
knoise = synth_def_context.set_context("EXCITER", "WhiteNoise.ar(0.2)").add(name="knoise")
knoise
[17]:
'knoise'
[18]:
# for testing, let's create a synth and stop after 1s
%scv x = Synth.new(^knoise, [\amp, 0.05, \freq, 100])
time.sleep(1)
%scv x.free
-> Synth('knoise' : 1000)
-> Synth('knoise' : 1000)

To delete an existing synthdef in sc you can use

synthDef.free(name)

(but don’t do it now as the synthdef is used for examples below)

Remove all unused placeholders from current_def by

[19]:
synth_def_context.reset()
print(f"SC code with context vars: \n {synth_def_context.current_def} \n")
synth_def_context.unset_remaining()
print(f"SC code with unset context vars: \n {synth_def_context.current_def} \n", )
SC code with context vars:

{ |out=0, amp=0.3, freq=440|
    var klank = DynKlank.ar(`[[1,2], [1,1], [1.4,1]], {{EXCITER}}, freq);
    Out.ar(out, amp*klank!^p_channels);
}

SC code with unset context vars:

{ |out=0, amp=0.3, freq=440|
    var klank = DynKlank.ar(`[[1,2], [1,1], [1.4,1]], , freq);
    Out.ar(out, amp*klank!^p_channels);
}

Here you see, that the placeholder {{EXCITER}} has been deleted. * With this method you make sure, that you don’t have any unused placeholders in the definition before creating a SynthDef. * Note, however, that your code might then not be functional…

Example creation of many SynthDefs

In some cases you want to create many SynthDefs with only a small change. You can use the SynthDefs object multiple time to do this. Here we want to create playbuf synthdefs for 1 to 10 channels: (Reuse of the synthdef object, which is defined above)

[20]:
playBufsynthDef = SynthDef("playbuf_", """
    { |out=0, bufnum=1, rate=1, loop=0, pan=0, amp=0.3 |
            var sig = PlayBuf.ar(^num_channels, bufnum,
                rate*BufRateScale.kr(bufnum),
                loop: loop,
                doneAction: Done.freeSelf);
            Out.ar(out, Pan2.ar(sig, pan, amp))
    }"""
)
[21]:
synthPlaybufs = {}
for num in [1,2,4,8]:
    synthPlaybufs[num] = playBufsynthDef.add(pyvars={"num_channels": num}, name=f"playbuf_{num}")

Now you can access via synthPlayBufs[2] to the 2-ch playbuf etc.

[22]:
synthPlaybufs[2]
[22]:
'playbuf_2'

Use-case: DynKlank Synths with controllable nr. of filters

A problem with synthdefs is that some parameters can only be set at compile time. E.g. * A DynKlank needs to know the maximum nr. of filters in its filter bank at SynthDef time. * A synth will need to know the channel count at SynthDef time

Contexts allow to define such synthDefs dynamically on demand.

The following code is a dynamic DynKlank whose data-controlled nr. of filters is determined via the SynthDef class. * nr of channels and nr. of filters in the filter bank are specified via pyvars * TODO: find a way how to set amps, rings, and harms on Synth.new

[23]:
scn.SynthDef
[23]:
sc3nb.sc_objects.synthdef.SynthDef
[24]:
synth_def = scn.SynthDef(name="myKlank", definition=r"""
{ |out=0, amp=0.3, freq=440|
    var klank, n, harms, amps, rings;
    harms = \harms.kr(Array.series(^p_nf, 1, 1));
    amps = \amps.kr(Array.fill(^p_nf, 0));
    rings = \rings.kr(Array.fill(^p_nf, 0.1));
    klank = DynKlank.ar(`[harms, amps, rings], {{EXCITER}}, freq);
    Out.ar(out, amp*klank!^p_channels);
}""")
[25]:
# now create a synth where exciter is Dust, with 10 filters and stereo
kdust = synth_def.set_context("EXCITER", "Dust.ar(80)").add(
    pyvars={"p_nf": 10, "p_channels": 2})
print(kdust)
myKlank
[26]:
x = scn.Synth(name=kdust,
              controls={"freq": 100,
                        "amp": 0.05,
                        "harms": [60,60,60],
                        "amps": [0.1,0.1,0.1],
                        "rings": [1, 0.4, 0.2],})

[27]:
x.set({"harms":[1,2,6], "amps": [0.1,0.1,0.1], "rings": [1, 0.4, 0.2]})
[27]:
<Synth(20002) 'myKlank' s {'freq': 100, 'amp': 0.05, 'harms': [1, 2, 6], 'amps': [0.1, 0.1, 0.1], 'rings': [1, 0.4, 0.2]}>
[28]:
# following syntax works the same:
# x.set(["harms",[1,2,6],"amps",[0.1,0.1,0.1],"rings",[1, 0.4, 0.2]])
[29]:
x.free()
[29]:
<Synth(20002) 'myKlank' f {'freq': 100, 'amp': 0.05, 'harms': [1, 2, 6], 'amps': [0.1, 0.1, 0.1], 'rings': [1, 0.4, 0.2]}>

Getting a SynthDesc

You can also query for a SynthDesc via sclang with the class method get_desc("synth_name")

Lets see for the SynthDefs included in sc3nb

  • SynthDef “s1” which is a discrete sound event with parameters

    • frequency freq

    • duration dur

    • attack time att

    • amplitude amp

    • number of harmonics num

    • spatial panning pan

[30]:
scn.SynthDef.get_description("s1")
[30]:
{'freq': SynthArgument(name='freq', rate='control', default=400.0),
 'amp': SynthArgument(name='amp', rate='control', default=0.30000001192092896),
 'num': SynthArgument(name='num', rate='control', default=4.0),
 'pan': SynthArgument(name='pan', rate='control', default=0.0),
 'dur': SynthArgument(name='dur', rate='control', default=0.4000000059604645),
 'att': SynthArgument(name='att', rate='control', default=0.009999999776482582),
 'curve': SynthArgument(name='curve', rate='control', default=-2.0)}
  • SynthDef “s2” which is a continuous synth with parameters

    • frequency freq

    • amplitude amp

    • number of harmonics num

    • spatial panning pan

    • Exponential lag lg

    • EnvGen gate gate, which allows the Node.release method

[31]:
scn.SynthDef.get_description("s2")
[31]:
{'freq': SynthArgument(name='freq', rate='control', default=400.0),
 'amp': SynthArgument(name='amp', rate='control', default=0.30000001192092896),
 'num': SynthArgument(name='num', rate='control', default=4.0),
 'pan': SynthArgument(name='pan', rate='control', default=0.0),
 'lg': SynthArgument(name='lg', rate='control', default=0.10000000149011612),
 'gate': SynthArgument(name='gate', rate='control', default=1.0)}
[32]:
sc.exit()
Quitting SCServer... [scsynth | reached EOF ]
Done.
Exiting sclang... [sclang | reached EOF ]
Done.
[ ]: