By the way, how on earth do you get the syntax coloring to work in this forum?

oh, thanks for that.
so for me, Object.addDependant is the key takeaway, with which you can make many things.
e.g., i think in the original question, two-way binding could be helpful.

re: syntax coloring, i dunno, i just wrapped the code in triple-backticks and it worked.

Thanks, triple-backticks did the trick for syntax colour.

On decoupling: Some years back I did foray into MVC and related patterns in SuperCollider but had a hard time deciding on how to structure certain parts of my application according to the pattern. For example, how does sequencers fit into the MVC pattern. Is a sequencer a controller? Is it part of the model? But you do not want too much logic in the model, right?

I ended up dumbing things down and using publish/subscribe via Object.addDependant which solved my major decoupling issues and also kind of inherently promoted structuring UI code well. So I mostly keep core, non-UI stuff in one place and spit out notification events via Object.changed letting other parts of the program listen and act upon those events however is needed. It works good enough for my small projects. I agree two way databinding would be better. It is probably a must for non-small projects.

On another note: SimpleController actually relies on Object.addDependant.

For publish/subscribe I can also recommend looking into NotificationCenter,

1 Like

hi. sorry for late response @zebra its been really hectic for me lately.

At that time i was thinking about using pbinds to control grid but now i decided to implement pbind related methods as a function on top of step-sequencer. Thought that getting all tracks/rows in a different routine for the sequence part would be more practical and it pretty much solved all the issues so far.

What i mean is some kind of embedded system rather than the MVP paradigm. Got myself a Kenton Killimax Mini to control both synthdef parameters and sequence changes, hell of a data routing/mapping tho.

Got some kind of polyrhythmic sequencer running right now, pretty primitive. But i believe pbinds are really versatile if can be controlled/augmented by grid.

Happy that this thread exists :slight_smile:

Hi,

I got something you might be interested in… the classic monome “step” app using pbinds, with grid and arc control. I just want to release a serialosc lib for SuperCollider I’ve been working on… sooon.

1 Like

Here’s a Pbind-based step sequencer with SerialOSCClient UI (grid and arc).

The SerialOSCClient UI code is crude. The core stuff is all right.

Dependancy support - as described in this thread - is used to decouple the UI from the core stuff.

I wanted to make a drag n drop UI for sample loading before releasing but I’ve been so busy I could not find time yet. For now you need to use ~load_sample.(samplenum, path) to load your samples.

:slight_smile:

1 Like

lovely track, lovely album! I remember listening to it a few weeks back being super excited about it. Now even more so seeing the code.
Thanks for sharing!

Agreed. Amazing stuff. I did not listen to it until now.

2 Likes

Genelec M030 over here

I posted this in a Muff’s SC thread. This is an interactive homework assignment that I turned in a few years back. This isn’t the most in-depth SC tutorial, but it shows a learning process in a build-up manner. Just copy-paste the entire thing into the SC editor.

Anything labelled “AC” is a comment from my professor, Andres Cabrera:

//240A Theory Paper
//SuperCollider Composition by Michael Hetrick
//Started 12.03.12
//Finished 12.xx.12

//In this interactive paper, I will be exploring generative strategies in Supercollider. I am writing this paper simultaneously with my learning process,
//so all code you see is written as I learn it.

//Recently, I've been working a lot with my hardware modular synth. One of my favorite modules is Maths by Make Noise.
//The main concept of Maths is a looping AD envelope with many other tricks. I have found it to be invaluable in making music.
//I'm starting off with some example code for the Decay UGen:

play({ Decay.ar(Impulse.ar(XLine.kr(1,50,20), 0.25), 0.2, PinkNoise.ar, 0) });

//Similarly, the Decay2 documentation contains a much more pleasant sine implementation.
{ Decay.ar(Impulse.ar(XLine.kr(1,50,20), 0.25), 0.2, FSinOsc.ar(600), 0)  }.play;

//Both of these examples use an XLine object to take the AD envelope from subsonic frequencies, through morphosis into AM synthesis.
//Annoylingly, both of these examples only use the left channel.
//While the morphosis is a very cool effect, I want to do something more rhythmic in stereo. I am starting with the following line:

{ Decay.ar(Impulse.ar([1.50, 1.25], 0.25), 0.3, FSinOsc.ar(600), 0)*0.3  }.play;

//This creates a short percussive loop in stereo. To achieve the stereo effect, I have used the [ , ] notation inside of the Impulse arguments.
//Next, I want to set up stereo notes:

{ Decay.ar(Impulse.ar([1.50, 1.25], 0.25), 0.3, FSinOsc.ar([600,300]), 0)*0.3  }.play;

//This is more interesting, but still quite boring. To offset this, I have set up three envelopes to be played simultaneously.

{ Decay.ar(Impulse.ar([1.50, 1.25], 0.25), 0.3, FSinOsc.ar(600), 0)*0.3  }.play;
{ Decay.ar(Impulse.ar([0.75, 1.75], 0.25), 0.3, FSinOsc.ar(300), 0)*0.3  }.play;
{ Decay.ar(Impulse.ar([2.50, 1.00], 0.25), 0.3, FSinOsc.ar(400), 0)*0.3  }.play;

//Triggering these simultaneously creates all sorts of rhythms as the patterns go in and out of sync.
//Okay, so now we need changing notes. It seems the best way to do this would be to create a SynthDef, and trigger each impulse manually:

SynthDef.new("blip", { |leftPitch = 440, rightPitch = 440, amp = 0.3, decay = 0.25, leftFreq=0.0, rightFreq=0.0|
    var sig;
	sig = Decay.ar(Impulse.ar([leftFreq, rightFreq], 0.0), decay, FSinOsc.ar([leftPitch, rightPitch]), 0)*amp;
    Out.ar(0, sig ! 2);    // sig ! 2 is the same as [sig, sig]
}).play;

x = Synth.new("blip");

// AC: Remember to free the synth! You can use the doneAction in envelope generators, but it can't be done here! (OK I see you've solved it below)

//That took a lot of trial and error! If Impulse's frequency is set to 0, it only releases one impulse. This now gives me the flexibility
//to sequence individual blips or full pulse streams. For example:

x =  Synth.new("blip",["leftPitch", 600, "rightPitch", 400]);   //blip chord
x =  Synth.new("blip",["leftPitch", 600, "rightPitch", 400, "leftFreq", 200, "rightFreq", 20, "amp", 0.1]); //Stereo AM synth

// AC: I'm trying to figure out why this sounds buzzy. Do you know? I suspect there is a sudden jump in amplitude when the Decay is retriggered.

//We learned about Routines in class, which are something I haven't tried before. Let's attempt to create a simple stereo sequence.

r = Routine({
	a =  Synth.new("blip",["leftPitch", 600, "rightPitch", 400]).yield;
	b =  Synth.new("blip",["leftPitch", 300, "rightPitch", 300]).yield;
	c =  Synth.new("blip",["leftPitch", 900, "rightPitch", 200]).yield;
	d =  Synth.new("blip",["leftPitch", 800, "rightPitch", 400]).yield;
	a.free; b.free; c.free; d.free;
});

r.next;
r.reset;
//Cool! We have two melodies (one on each channel). Alternatively, we can use a task:

(
t = Task({
    loop {
        [600, 300, 900, 800].do({ |seqFreq|
            Synth("blip", [ amp: 0.2, leftPitch:seqFreq]);
            0.125.wait;
        });
    }
}).play;
)

t.stop;
t.play;

//I like that! I'm wondering what will happen if I do a double do loop:
(
t = Task({
    loop {
		[400, 300, 200, 300].do({ |rightSeqFreq|
			[600, 300, 900, 800].do({ |leftSeqFreq|
				x = Synth("blip", [ amp: 0.2, leftPitch:leftSeqFreq, rightPitch:rightSeqFreq]);
				0.125.wait;
				x.free;
			});
		});
    }
}).play;
)

t.stop;
t.play;

//I turned the Synth part into "x" so that I could free it after every loop. I did this because the RAM and CPU usage were
//growing with every repeat. This is now an efficient, looping sequence!

//The SC sequencing tutorial is now trying to convince me to use patterns. This seems like it would be a cleaner way of sequencing
//longer patterns with more variation.
(
var leftNoteSequence, rightNoteSequence;
leftNoteSequence = Pseq([600, 300, 900, 800, 400, 600],inf).asStream;
rightNoteSequence = Pseq([600, 400, 300, 200, 400],inf).asStream;
t = Task({
    loop {
				x = Synth("blip", [ amp: 0.2, leftPitch:leftNoteSequence.next, rightPitch:rightNoteSequence.next]);
				0.125.wait;
				x.free;
    }
}).play;
)

//Now we're talking! Because of the uneven PSeq size, I now have a 30-event sequence. Let's see if we can do something cool
//with changing the frequency of the Impulse generator.
//First, let's redefine the SynthDef to include stereo amplitude control:
SynthDef.new("blip", { |leftPitch = 440, rightPitch = 440, ampL = 0.3, ampR = 0.3, decay = 0.25, leftFreq=0.0, rightFreq=0.0|
    var sig;
	sig = Decay.ar(Impulse.ar([leftFreq, rightFreq], 0.0), decay, FSinOsc.ar([leftPitch, rightPitch]), 0)*[ampL, ampR];
    Out.ar(0, sig ! 2);    // sig ! 2 is the same as [sig, sig]
}).play;

//Now, let's make a new sequence with pauses

(
var leftNoteSequence, rightNoteSequence;
var leftAmpSequence, rightAmpSequence;
leftNoteSequence = Pseq([600, 300, 900, 800, 400, 600],inf).asStream;
rightNoteSequence = Pseq([600, 400, 300, 200, 400],inf).asStream;
leftAmpSequence = Pseq([0.3, 0.0, 0.0, 0.3, 0.2, 0.1],inf).asStream;
rightAmpSequence = Pseq([0.3, 0.0, 0.3, 0.0, 0.0, 0.3, 0.3, 0.1, 0.3],inf).asStream;
t = Task({
    loop {
				x = Synth("blip", [ ampL: leftAmpSequence.next, ampR: rightAmpSequence.next, leftPitch:leftNoteSequence.next, rightPitch:rightNoteSequence.next]);
				0.125.wait;
				x.free;
    }
}).play;
)

//Cool! Here it is with different Impulse frequencies:
(
var leftNoteSequence, rightNoteSequence;
var leftAmpSequence, rightAmpSequence;
var leftImpSequence, rightImpSequence;
leftNoteSequence = Pseq([600, 300, 900, 800, 400, 600],inf).asStream;
rightNoteSequence = Pseq([600, 400, 300, 200, 400],inf).asStream;
leftAmpSequence = Pseq([0.3, 0.0, 0.0, 0.3, 0.2, 0.1],inf).asStream;
rightAmpSequence = Pseq([0.3, 0.0, 0.3, 0.0, 0.0, 0.3, 0.3, 0.1, 0.3],inf).asStream;
leftImpSequence = Pseq([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0, 12.0],inf).asStream;
rightImpSequence = Pseq([0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 14.0, 0.0, 0.0, 0.0, 0.0],inf).asStream;
t = Task({
    loop {
				x = Synth("blip", [ ampL: leftAmpSequence.next, ampR: rightAmpSequence.next, leftPitch:leftNoteSequence.next, rightPitch:rightNoteSequence.next, leftFreq: leftImpSequence.next, rightFreq: rightImpSequence.next]);
				0.125.wait;
				x.free;
    }
}).play;
)

//A bit clicky. Let's redefine our Synth to use Decay2, which has an attack time.
// AC: This is the solution...
(
SynthDef.new("blip", { |leftPitch = 440, rightPitch = 440, ampL = 0.3, ampR = 0.3, decay = 0.25, leftFreq=0.0, rightFreq=0.0|
    var sig;
	sig = Decay2.ar(Impulse.ar([leftFreq, rightFreq], 0.0),  0.01, decay, FSinOsc.ar([leftPitch, rightPitch]), 0)*[ampL, ampR];
    Out.ar(0, sig ! 2);    // sig ! 2 is the same as [sig, sig]
}).play;
)
//And now, let's use a shorter decay time, and let's change the length of time between loops

(
var leftNoteSequence, rightNoteSequence;
var leftAmpSequence, rightAmpSequence;
var leftImpSequence, rightImpSequence;
var loopLengthSequence;
leftNoteSequence = Pseq([600, 300, 900, 800, 400, 600],inf).asStream;
rightNoteSequence = Pseq([600, 400, 300, 200, 400],inf).asStream;
leftAmpSequence = Pseq([0.3, 0.0, 0.0, 0.3, 0.2, 0.1],inf).asStream;
rightAmpSequence = Pseq([0.3, 0.0, 0.3, 0.0, 0.0, 0.3, 0.3, 0.1, 0.3],inf).asStream;
leftImpSequence = Pseq([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0, 12.0],inf).asStream;
rightImpSequence = Pseq([0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 14.0, 0.0, 0.0, 0.0, 0.0],inf).asStream;
loopLengthSequence = Pseq([0.125, 0.125, 0.125, 0.25, 0.25, 0.125],inf).asStream;
t = Task({
    loop {
				x = Synth("blip", [ ampL: leftAmpSequence.next, ampR: rightAmpSequence.next, leftPitch:leftNoteSequence.next, rightPitch:rightNoteSequence.next, leftFreq: leftImpSequence.next, rightFreq: rightImpSequence.next, decay: 0.15]);
				loopLengthSequence.next.wait;
				x.free;
    }
}).play;
)

//Now that I have a cool melodic sequence, I should put together a hi-hat pattern
(
SynthDef.new("hihat", { |ampL = 0.3, ampR = 0.3, decay = 0.10, leftFreq=0.0, rightFreq=0.0|
    var sig;
	sig = Decay2.ar(Impulse.ar([leftFreq, rightFreq], 0.0),  0.01, decay, WhiteNoise.ar, 0)*[ampL, ampR];
    Out.ar(0, sig ! 2);
}).play;
)

//Adding it to the melodic sequence:
(
var hiHatSequence;
var leftNoteSequence, rightNoteSequence;
var leftAmpSequence, rightAmpSequence;
var leftImpSequence, rightImpSequence;
var loopLengthSequence;
leftNoteSequence = Pseq([600, 300, 900, 800, 400, 600],inf).asStream;
rightNoteSequence = Pseq([600, 400, 300, 200, 400],inf).asStream;
leftAmpSequence = Pseq([0.3, 0.0, 0.0, 0.3, 0.2, 0.1],inf).asStream;
rightAmpSequence = Pseq([0.3, 0.0, 0.3, 0.0, 0.0, 0.3, 0.3, 0.1, 0.3],inf).asStream;
leftImpSequence = Pseq([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0, 12.0],inf).asStream;
rightImpSequence = Pseq([0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 14.0, 0.0, 0.0, 0.0, 0.0],inf).asStream;
loopLengthSequence = Pseq([0.125, 0.125, 0.125, 0.25, 0.25, 0.125],inf).asStream;
hiHatSequence = Pseq([0.3, 0.1, 0.3, 0.0, 0.3, 0.0, 0.3, 0.0, 0.0, 0.1, 0.1, 0.0, 0.3],inf).asStream;
t = Task({
    loop {
				x = Synth("blip", [ ampL: leftAmpSequence.next, ampR: rightAmpSequence.next, leftPitch:leftNoteSequence.next, rightPitch:rightNoteSequence.next, leftFreq: leftImpSequence.next, rightFreq: rightImpSequence.next, decay: 0.15]);
		        y = Synth("hihat", [ ampL: hiHatSequence.next, ampR: hiHatSequence.next]);
				loopLengthSequence.next.wait;
				x.free; y.free;
    }
}).play;
)
16 Likes

Thanks, I appreciate that :slight_smile:

this is super awesome and I discovered it during a SC research spree two months ago

thanks for re-posting for lines peeps

Ok, stupid question time. Just going through some tutorials, and I ended up with this code:

(
var tempoclock = TempoClock(120/60);

{
inf.do {
Synth(\event, [\freq, 60.midicps]);
tempoclock.beatDur.wait;
};
}.fork(tempoclock)
)

Why does the synth trigger at what seems to be 240 bpm instead of 120 bpm? If I change the TempoClock argument to 60/60 it’s 1/4 of the speed of this.

Yes, I know there’s other / better ways to sequence stuff but I’m just scratching my head trying to figure out why this is working the way it is…

TL/DR:
if you want to specify durations in absolute time, you don’t need a Clock.
if you’re using a Clock, you probably want to specify durations in musical time.


i think it’s going something like this.

the fork method, with a Clock as argument, wraps the function in a new Routine and plays it on the Clock

fork { arg clock, quant, stackSize;
        ^Routine(this, stackSize).play(clock, quant);
    }

the value given to wait or yield is interpreted by the Clock as the number of beats to wait before rescheduling.

so i think what you want is either this (wait one beat and modify the tempo of the clock):


(
var tempoclock = TempoClock(120/60);
{
	inf.do {
		Synth(\event, [\freq, 60.midicps]);
		1.0.wait;  // <<<<----- 
	};
}.fork(tempoclock)
)

or this (use the default TempoClock and modulate the wait time):

(
var beatDur = 0.5;
{
	inf.do {
		Synth(\event, [\freq, 60.midicps]);
		beatDur.wait;
	};
}.fork; // <<<---
)

Ok, I see now.

So if my understanding is correct, when you schedule a task against TempoClock the time is expressed in beats. When you schedule against a SystemClock, it’s expressed in seconds.

Correct.

The problem is that beatDur returns the duration in seconds of a current whole beat, and as zebra wrote you typically specify your wait durations in beats for functions forked to TempoClocks.

super pedantic clarification:

when you use Function.fork with no clock argument, (or Routine.play) it still uses a (default) TempoClock. you can change the default TempoClock. you can use static methods to access the default TempoClock.

one reason this is significant is that TempoClock has meter and other stuff that SystemClock doesn’t:

s = Server.default;
s.waitForBoot({

    t = TempoClock(5);
    TempoClock.default = t;

    {
        SynthDef.new(\event, { arg freq;
            Out.ar(0, SinOsc.ar(freq) * EnvGen.ar(Env.perc, doneAction:2) * 0.1);
        }).send(s);
        s.sync;
        
        TempoClock.beatsPerBar = 5;
        /// these do the same thing:
        // TempoClock.default.beatsPerBar = 5;
        // t.beatsPerBar = 5;

        inf.do {
            Synth(\event, [\freq, (60 + (3 * TempoClock.beatInBar)).midicps]);
            1.0.wait;
        };

    }.fork;
});

and you can, say, modulate everything on the default clock at once:

s = Server.default;
s.waitForBoot({

    t = TempoClock(5);
    TempoClock.default = t;

    {
        SynthDef.new(\event, { arg freq, pan=0;
            var snd = SinOsc.ar(freq) * EnvGen.ar(Env.perc, doneAction:2) * 0.1;
            Out.ar(0, Pan2.ar(snd, pan));
        }).send(s);
        s.sync;

        TempoClock.beatsPerBar = 5;

        {
            inf.do {
                Synth(\event, [\pan, -0.5, \freq, (60 + (3 * TempoClock.beatInBar)).midicps]);
                1.0.wait;
            };
        }.fork;

        {
            inf.do {
                Synth(\event, [\pan, 0.5, \freq, (69 + (5 * TempoClock.beatInBar)).midicps]);
                0.75.wait;
            };
        }.fork;


    }.fork;
});

w = Window.new("tempo", Rect(0, 0, 100, 300));
Slider.new(w, w.bounds).action_( { |sl|
    TempoClock.default.tempo_(sl.value * 20.0)
});
w.front;

i’m trying to think of a scenario where it’s better to explicitly schedule something on the SystemClock. in theory:

  • if you’re calling stuff that isn’t thread safe.
  • if you want to make sure its timing can never be messed with by something like the 2nd example above.

This is very typical of SuperCollider. It relies very (too?) much on implicit defaults. That was tricky for me as a newcomer to SuperCollider.

For instance, for Events you can do this and get a sound (of course assuming server is booted).

().play;

Adding some other supported property to the event will override the default for that property, but use defaults for other stuff.

(degree: 3).play;

The same applies to Pbinds. Without arguments you still get a sequence - it uses the default synthdef, note, duration:

Pbind().play // press Cmd-. to stop the sequence

Override one argument, otherwise use defaults:

Pbind(*[degree: Pseq([0, 3, 7, 10], inf)]).play

Override two arguments, otherwise use defaults:

Pbind(*[
	degree: Pseq([0, 3, 7, 10], inf),
	dur: Pseq([0.15, 0.1], inf)
]).play
)```

Another default: All Pbinds are by default also played using the default TempoClock for tempo, so changing its tempo will affect the pattern:

```TempoClock.default.tempo*60; // post current tempo in bpm
TempoClock.default.tempo = 80/60; // change pattern tempo to 80 bpm
TempoClock.default.tempo = 100/60; // ... 100 bpm
TempoClock.default.tempo = 120/60; // ... 120 bpm```

[quote="zebra, post:27, topic:3185"]
i'm trying to think of a scenario where it's better to explicitly schedule something on the SystemClock
[/quote]

SystemClocks and TempoClocks have the same priority so I believe it's mostly a matter of whether you want to be able to determine / change tempo dynamically (use a TempoClock) or just schedule by regular time.

AppClock, a third option, is of lower priority and typically used for UI updates. That's why you as a newcomer also run into errors that mean having to ```{}.defer``` stuff that's scheduled on a SystemClock or TempoClock that is supposed to update GUI widgets.

Since SystemClock has no variable tempo I believe only one exists and is used but with TempoClocks you can create as many as you want if you need multiple apps with differing tempos.

How nice we got SuperCollider discussions going on here...

SystemClocks and TempoClocks have the same priority

this is getting deeper into the dark waters of the SC scheduler implementation, which i’m not very familiar with.

but, i am pretty sure that creating a TempoClock always attempts to create a new thread as well:

whereas SystemClock (which is singleton) always runs on the interpreter’s main process (calls schedRunFunc() directly in that same source, instead of passing the function to a pthread.)

i am 90% sure i’ve run into situations where things failed to work as expected in a TempoClock. (stuff like filesystem, network, pipes, serial drivers…) and needed to be scheduled explicitly on the SystemClock. presumably because of pthreads.

but i could be wrong about this. there is probably better information to be had on this in the sc-users, sc-devel mailing lists.


It relies very (too?) much on implicit defaults

totally agreed. can be very confusing. i spent many years in ignorance of what i was actually doing in common SC patterns. ctl-D is always informative.

You’re more experienced in low level stuff and probably right in that they’re not of the same OS priority. Thanks for correcting me.

Let me rephrase. What I meant was that SystemClock and TempoClock are for timing critical stuff, whereas AppClock is not.