How does SuperCollider Smoothen the sound? It sounds way too different than Pd and without clicks

nope, let me show you what I said that’s easy to do in Pd. Here I bang the envelope and record it into an array, so I get sample by sample recorded into the array. Then I simply open the list view of the array and check the values for all samples…

This polling is just collecting samples at a specific interval, right? so not the same thing

Then, like this?

/* Env.ar */

(
{
	var duration = 0.2;
	b = Buffer.alloc(s, s.sampleRate * duration);
	c = [];
	s.sync;
	{
		var env = Env([1, 0], [duration]).ar;
		RecordBuf.ar(env, b, loop: 0, doneAction: Done.freeSelf);
	}.play;
	(duration * 1.1).wait;
	b.loadToFloatArray(action: { |array| { array.plot }.defer; "done".postln });
	b.getn(0, b.numFrames, { |msg| c = msg })
}.fork(clock: AppClock)
)

c.size // -> 9600
c.size == b.numFrames
c[0] // -> 1.0
c.last // -> 0.00010418156307423


/* Env.kr */

(
{
	var duration = 0.2;
	d = Buffer.alloc(s, (s.sampleRate / s.options.blockSize * duration).asInteger);
	e = [];
	s.sync;
	{
		var env = Env([1, 0], [duration]).kr;
		RecordBuf.kr(env, d, loop: 0, doneAction: Done.freeSelf);
	}.play;
	(duration * 1.1).wait;
	d.loadToFloatArray(action: { |array| { array.plot }.defer; "done".postln });
	d.getn(0, d.numFrames, { |msg| e = msg })
}.fork(clock: AppClock)
)

e.size // -> 150
e.size == d.numFrames
e[0] // -> 1.0
e.last // -> 0.0066666812635958

See also:

/* Env to Signal of sample rate size */

(
var duration = 1;
e = Env([1, 0], [1]);
f = e.asSignal((s.sampleRate * duration).asInteger)
)

f.plot
f.size // -> 48000
f[0] // -> 1.0
f[10] // -> 0.99979168176651
f.last // -> 1.1102230246252e-16
f.last == f[f.size - 1]
f.do {|value, index| [index, value].postln }


/* Env to Signal of control rate size */

(
var duration = 1;
g = Env([1, 0], [1]);
h = g.asSignal((s.sampleRate / s.options.blockSize * duration).asInteger)
)

h.plot
h.size // -> 750
h[0] // -> 1.0
h[10] // -> 0.98664885759354
h.last // -> 0.0
h.last == h[h.size - 1]
h.do {|value, index| [index, value].postln }

ALSO:

(
var env = Env.perc(0.001, 0.01); 
{ EnvGen.ar(env) }.plot(0.015, bounds: Rect(500, 200, 400, 300));
{ EnvGen.kr(env) }.plot(0.015, bounds: Rect(500, 500, 400, 300));
Env.perc(0.001, 0.01).test.plot
)

Have another look at the code, including comments: “assuming signal is ar – if it’s kr, write RecordBuf.kr

It isn’t valid to record ar for a kr input. Probably SC should disallow this with an error, but it doesn’t.

If you want to record at ar the result of upsampling kr to ar, then you have to write the conversion explicitly. K2A is not optional here.

(
a = {
var signal = Env([0.11, 1, 0], [0, 30], -225).kr(doneAction:2);
// RecordBuf = tabwrite~ basically
// assuming signal is ar – if it’s kr, write RecordBuf.kr()
RecordBuf.ar(K2A.ar(signal), b, loop: 0, doneAction: 2);
Silent.ar(1);
}.play;
)

Also, it will help others to help you better if you post code blocks formatted as code. See the </> “preformatted text” button in the toolbar, or use markdown style backticks:

```
... your code...
```

This nicety doesn’t matter on the pd forum, but it makes a difference when it’s important to preserve the integrity of posted code.

hjh

Sort of. Kr samples exist only at block boundaries.

Kr signals are (often/usually) automatically up-sampled to ar when serving as an input to an audio rate operator. In general this is true for parameter inputs (filter frequency, reciprocal of Q, etc) but not eg for filter or recorder audio inputs. This up-sampling uses linear interpolation. If it didn’t, you’d get really gross zipper noise when multiplying audio by a kr envelope.

hjh

Hi, I think we’re overcomplicating and dealing with too many options. The task is simple: record the generated audio output from a control rate envelope into a buffer and check the sample values.

The envelope in question is this,

Env([0.11, 1, 0], [0, 30], -225).kr

but there are others I wanna check

prko I tried recording the 1st 10ms in your example with this


({	var dur = 0.01; // 10 ms
	d = Buffer.alloc(s, (s.sampleRate / s.options.blockSize * dur).asInteger);
	e = [];
	s.sync;
	{
		var env = Env([0.11, 1, 0], [0, 30], -225).kr;
		RecordBuf.kr(env, d, loop: 0, doneAction: Done.freeSelf);
	}.play;
	(dur * 1.1).wait;
	d.loadToFloatArray(action: { |array| { array.plot }.defer; "done".postln });
	d.getn(0, d.numFrames, { |msg| e = msg })
}.fork(clock: AppClock)
)

e.size // 6 (blocks?)
e.size == d.numFrames
e[0] 
e.last

I just got a size of 6 values. Those seem to be the number of blocks, not samples, so it just collects the samples at block boundaries…

jamshark70

So, I tried this


(var ms = 10; // size in ms
var samps =  (ms / 1000 * s.sampleRate).round;
b = Buffer.alloc(s, samps))

(a = {var env = Env([0.11, 1, 0], [0, 30], -225).kr;
RecordBuf.ar(K2A.ar(env), b, loop: 0);
Silent.ar(1)}.play)

b.plot

I don’t access to the sample values, so it’s not like recording and being able to check the values. nonetheless, I can see that the output is what you described.

The ideal would be to check the sample values, I’d like to be able to learn how to do that, but this is probably fine and ok for now… The code doesn’t make much sense to me, but it’s ok. One question I have is that I tried this

(a = {var env = EnvGen.kr(Env.new([0.11, 1, 0], [0, decay], -225));
RecordBuf.ar(K2A.ar(env), b, loop: 0);
Silent.ar(1)}.play)

But it did not work. Why? This is what we have in that bass drum code, is it really supposed to be equivalent as the other code bit that works?

And yeah, this plots it differently, why?

{EnvGen.kr(Env.new([0.11, 1, 0], [0, 30], -225))}.plot(0.01);

With .plot, it is impossible to correctly display the values of audio samples over 30 seconds. One second contains 44,100 samples, for example. If you use HDMI, it may contain 48,000 samples per second. The pixel resolutions of common displays are as follows:

  • Full HD: 1920 × 1080
  • QHD: 2560 × 1440
  • UHD / 4K: 3840 × 2160
  • DCI 4K: 4096 × 2160
  • 8K UHD: 7680 × 4320

A time span of 0.01 ms contains approximately 441 samples (or 480 samples with HDMI).

Anyway:

(
{	var dur = 1; // or 30
	d = Buffer.alloc(s, s.sampleRate * dur);
	e = [];
	s.sync;
	{
		var env = Env([0.11, 1, 0], [0, 30], -225).kr;
		// var env = Env([0.11, 1, 0], [0, dur], -225).kr;
		RecordBuf.ar(K2A.ar(env), d, loop: 0, doneAction: Done.freeSelf);
	}.play;
	(dur * 1.1).wait;
	
	// Fetch buffer data using loadToFloatArray to avoid OSC size limits
	d.loadToFloatArray(action: { |array| 
		e = array; 
		{ array.plot }.defer
	});
}.fork
)

e.size //-> 48000 (on my end)
e.size == d.numFrames
e[0] 
e.last
e.plot

Env([0.11, 1, 0], [0, 0.01], -225).test.plot
Env([0.11, 1, 0], [0, 1], -225).test.plot
Env([0.11, 1, 0], [0, 30], -225).test.plot

Is this what you want to achieve?
Please change the width of both plot windows.

sure, I never meant to be able to view the sample values on the graph. The graph is a visual cue, and I was also checking 10ms or 100ms :slight_smile:

Your code works now for generating the same plot as jamshark70, nice! Here it is…

And we can have access to the values in the array. Great! I guess I can have a for loop to plot them all in the console :wink: but don’t worry, all the help I got here is already quite meaningful and helpful. Thanks!

With this plot it makes it clear why “.ar” gives a different result, as I believe it immediately jumps to “1” and promotes a quite harsher attack/click.

Now, to go back to the original question of the thread, besides these details with control rate and stuff, which makes it hard to port to Pd, should there be any real difference in how the computer makes sound and how the SuperCollider engine works? :slight_smile:

The ‘play’ fadein was something that I missed completely, but I have always felt that SC does sound a bit different and smoother somehow… of course the ugens/objects are a bit different and never exactly the same, but it’s beyond that. Also, of course this can be just some wrong perception, and maybe SC people just make “nicer sounding” patches than Pd people :stuck_out_tongue: haha

Maybe the size of the wavetable causes the difference. See the help for each object:

  • pd’s osc~:
    Up to version 0.54, [osc~] relied on a 512-point cosine table, which was adequate for most “computer music” applications in the 90s with a signal-to-noise ratio of about 110 dB. The default table size is now increased to 2048 points, improving signal-to-noise by an additional 36 dB. To get the original 512-point tables you can run Pd with compatibility level set to 0.54.
  • SuperCollder’s SinOsc:
    Generates a sine wave. Uses a wavetable lookup oscillator with linear interpolation. Frequency and phase modulation are provided for audio-rate modulation. Technically, SinOsc uses the same implementation as Osc except that its table is fixed to be a sine wave made of 8192 samples.

For one cycle of a sine/cosine wave, SuperCollider uses 8192 samples, while Pd versions above 0.54 use 2048 samples (and Pd ≤ 0.54 uses 512 samples).

I hear no difference between vanilla’s [osc~] and my oscillator [else/sine~] which has a table size of 16384 :slight_smile: I don’t hear differences in sines from SC either. Anyway, it’s a general perception and I don’t have good examples… and like I said I know some of the differences between objects/implementations, but it’s something I feel it’s beyond that, and it could be just psychological and I’m just wrong in how I feel…

One factor might be multichannel expansion. A single oscillator tends to sound cold, sterile, maybe even a little harsh. Detuning softens this perception.

SC:

// if 'detun' is a parameter where 1.0 = no detuning,
// and 0.02.midiratio = +/- 2 cents
var detunedOscSum = Saw.ar(freq * Array.fill(7, { detun ** Rand(-1, 1) })).sum;

… erm, that’s it. It’s so easy to implement this. (BTW why ** Rand(-1, 1)? Because this way you can modulate on the fly. If you did something like ExpRand(detun.reciprocal, detun) then you would be stuck with the detuning factor that the synth started with.)

It used to be hard in Max, but the multi-channel wrapper makes it a lot easier. (Although, tbh, I disagree with their use of linear random numbers for detuning. This will bias the pitch slightly toward the high side. In my approach, the pitch center [geometric mean] of the detuning factors will tend toward 1.0.)

But “mc” seems relatively new (maybe 2021?). I’m not sure how widely it’s used to fatten up patches.

In Pd, “mc” support seems… spotty? ELSE lib (cheers to you) seems to be somewhat consistent about supporting multichannel oscillators… so in that sense, users of ELSE can do it just about as easily as in Max, although as far as I can see, you have to roll your own randomizer. The core pd-vanilla objects… do they make a similar guarantee? And I’m pretty sure other external packs will take some time to catch up. (TBH I haven’t dug into multichannel in Pd very much, so please correct me where I’m wrong.)

In any case, because it’s so easy to detune oscillators, I do it a lot – in just about every instrument I routinely play. The cost to write it is so low, there’s no reason not to include it. If the tool makes it harder to detune, then users do it less, and the colder single-oscillator sound becomes just part of the sound of the software or its community… “maybe SC people just make ‘nicer sounding’ patches than Pd people”: I would argue that SC’s way of representing synthesis graphs makes it easier to make nicer-sounding patches.

hjh

ELSE is over 250 objects with mc support and counting, oscillators were in the first batch. I have also created several objects that handle and generate lists like the random deviation and stuff

I also have a modular system that is polymorphic in the vcv style thanks to mc

I have to say this is nothing about what the tool provides you to do things… I am pretty aware if a SC patch is using MC expansion or not. I have to stress this is about how it ‘SOUNDS’, the synthesis engine… and I have felt that when porting other simple patches… and, honestly, I was happy using [clone] as an alternative for ‘multichannel’ crazy additive synthesis with random stuff. I was never hitting a wall when porting SC patches in a sense that I couldn’t possibly do the same in Pd. In fact, I do feel some stuff is really hard to make in SC, like simply ploting audio in an array to check it :wink:

So I was talking about somethin like trying to port this bassdrum patch, or other simple things… and it can be just the difference in implementation of an objet like the reverb, and things that we don’t have a real exact clone of or something.

As for the patch I was struggling with in here, I was having a real hard time, but now that I realized about the play deal and how those control rate objects work, I was able to perfectly port it. By the way, I also had to port Limiter, as I didn’t have one anyway.

The original patch was a bit clumsy as pointed, but I was also able to rewrtie it in a more sane way and use ‘.ar’ for the envelopes. I do now have a linear ramp rise instead of that nonsense artifacts from using ‘.kr’ and I am now also using better values, like 1 second instead of 30!

this was the original

({ // original
    var decay = 30, amp = 2, tone = 50;
    var env = Env([0.11, 1, 0], [0, decay], -225).kr;
    var trienv = Env([0.11, 0.6, 0], [0, decay], -230).kr;
    var fenv = Env([tone*7, tone*1.35, tone], [0.05, 0.6], -14).kr;
    var pfenv = Env([tone*7, tone*1.35, tone], [0.03, 0.6], -10).kr;
    var sig = SinOsc.ar(fenv, pi/2) * env;
    var sub = LFTri.ar(fenv, pi/2) * trienv * 0.05;
    var punch = HPF.ar(SinOsc.ar(pfenv, pi/2) * env * 2, 350);
	(Limiter.ar((sig + sub + punch) * 2.5, 0.5) * amp)!2;
}.play(fadeTime: 0))

this is my revision

({ // bass drum
    var decay = 1, amp = 2, tone = 50;
	var env = Env([0.11, 1, 0], [0.002, decay], ['lin', -7.4]).ar;
    var trienv = Env([0.11, 0.6, 0], [0.002, decay], ['lin', -7.44]).ar;
    var fenv = Env([tone*7, tone*1.35, tone], [0.05, 0.6], -14).ar;
    var pfenv = Env([tone*7, tone*1.35, tone], [0.03, 0.6], -10).ar;
    var sig = SinOsc.ar(fenv, pi/2) * env;
    var sub = LFTri.ar(fenv, 1) * trienv * 0.05;
    var punch = HPF.ar(SinOsc.ar(pfenv, pi/2) * env * 2, 350);
	(Limiter.ar((sig + sub + punch) * 2.5, 0.5) * amp)!2;
}.play(fadeTime: 0))

here’s the Pd outout

And, well, in the end, I guess there’s nothing really that should make SC sound significantly different and I guess I can forget about this :slight_smile: And well, to be honest, I guess I have the same kind of feeling with Csound pacthes and stuff… maybe I should forget about wondering if it has any ‘secret sauce under the hood’ as well.

Sure, I wasn’t saying that Pd can’t detune oscillators – of course it can. I was trying to say that, when something is more laborious to do in a particular environment, then the proportion of users willing to push that technique through to its conclusion becomes smaller. Then, as a matter of the culture around a synthesis environment, that technique will occupy a smaller space within the culture. The fact that a power user such as yourself can port advanced usages doesn’t mean the average user is doing so.

Part of it is user education, but the other part of it is about features that reduce friction. For myself, I tend not to attempt as complex stuff in Max or Pd. A large part of that is just the physical labor of moving between keyboard and mouse/touchpad: it’s physically easier to move faster in SC. (Also SC has a lot of conveniences – in Pd, I can’t live without my [linexp] / [linexp~] abstractions.)

It’s all YMMV. What works for me may be painful for someone else.

hjh