Fake resonance , aka windowed sync

I am trying to impleement this in supercollider using the basic modules but it sounds rather awfull
Here’s the reaktor implementation , sounds smooth as butter
I colour coded the equivalent variables in reaktor and here’s an audio file >
I’m sure it has something to do with the offset of the master sync providing the windowing function ,( in supercollider it stays bipolar ) and obvious sync discontinuaties
https://app.box.com/s/cgeaq1ufal51vernxg6fqbtccwi12e3m

(
{
	var sig,sig1,sig2,freq,ampenv,pitchenv,peamount;
	pitchenv=EnvGen.kr(Env([0,1,0],[0.001,2],[0,0]),doneAction:2);
	peamount=500;
	ampenv=     EnvGen.kr(Env([0,1,0],[0.001,2],[0,0]),doneAction:2);
	freq=    200;
	sig1=    Saw.ar(freq,mul:-0.53,add:0.5);
	sig2=    SinOsc.ar(freq+(pitchenv*peamount),phase:sig1,mul:1);
	sig=(sig1*sig2)**2;
	sig=sig*ampenv!2;
}.
play
)

quick catch: phase in sc is in radians so should range ±pi

Sure , but since the phase of the sineosc (sig2) has to be controlled by the master osc(which is sig1) ,so phase:Sig 1 , or do you mean
phase :sig1*pi ?..which doesn’t give astisfying reults either

Still no luck achieving in what I want
Help is appreciated , see first post for audio example

In your Reaktor patch, you have the output of sig1 patch into the Snc parameter of Sync Sine, which iirc resets the phase when there is a zero crossing in the input. There is no equivalent parameter for SinOsc - this is why you’re not hearing anything coherent.
If you want comparable behavior, you can use something like Sweep - which DOES reset when it’s input has a zero crossing (the trig parameter) - to drive the phase of your SinOsc.

Something like this seems closer to what you’re going for:

(
{
	var sig, sig1, sig2, freq, ampenv, pitchenv, peamount, sig2Phase;
	
	peamount = 150;
	freq = 90;

	pitchenv = EnvGen.ar(
		Env([0, 1, 0], [0.001, 2], [0, 0]), 
		doneAction:2
	);
	
	ampenv = EnvGen.kr(
		Env([0, 1, 0], [0.001, 2], [0, 0]),
		doneAction:2
	);
	
	sig1 = SinOsc.ar(
		freq,
		mul:-0.53,
		add:0.5
	);
	// sweep slope should be the frequency of your phase change - not which of the two freqs are more
	// appropriate here, but they both sound closer to your sound file
	sig2Phase = Sweep.ar(sig1, (freq + (pitchenv * peamount)));
	
	sig2 = SinOsc.ar(
		freq + (pitchenv * peamount), 
		phase: 2pi * sig2Phase
	);
	sig = (sig1 * sig2)**2;
	sig = sig * ampenv ! 2;
}.play
)

I totally forgot about the sync part
Too bad the sine osc doesn’t have a sync input , it does feel a bit cumbersome to achieve sync duties

For hard sync, wouldn’t freq be 0 and the SinOsc would be driven by phase?

What’s the concrete benefit over driving the phase with a resetting linear increment?

Edit: perhaps this technique? formant_window

(
{
	var sync = LFTri.ar(250);
	var phase = Sweep.ar(sync, 300);
	var synced = SinOsc.ar(0, (phase % 1) * 2pi);
	[sync, synced, synced * sync]
}.plot;
)

(
{
	var sync = LFTri.ar(250);
	var phase = Sweep.ar(sync, SinOsc.kr(0.1).exprange(280, 2000));
	var synced = SinOsc.ar(0, (phase % 1) * 2pi);
	dup(LeakDC.ar(synced * sync) * 0.1)
}.play;
)

The latter does have a bit of formant-shiftyness going on.

hjh

With the envelopes:

(
SynthDef(\windowsync, { |out, gate = 1, freq = 440, amp = 0.1,
	syncEgTop = 8, syncRatio = 2, syncDcy = 0.5|
	var syncEg = EnvGen.kr(Env([syncEgTop / syncRatio, 1], [syncDcy], \exp));
	var eg = EnvGen.kr(Env.adsr(0.01, 0.3, 0.6, 0.1), gate, doneAction: 2);
	var fundamental = LFTri.ar(freq);
	var syncFreq = freq * syncRatio * syncEg;
	// note, Phasor here is behaving like the Sweep above (retrigger behavior)
	// but Phasor loops around its range, eliminating the need for '% 1'
	var syncPhase = Phasor.ar(fundamental, syncFreq * SampleDur.ir, 0, 1, 0);
	var sig = SinOsc.ar(0, syncPhase * 2pi) * fundamental;
	Out.ar(out, (sig * (amp * eg)).dup);
}).add;
)

(instrument: \windowsync, freq: 36.midicps, syncEgTop: 20).play;

(Maybe needs a little saturation?)

hjh

That’s what I thought in the beginning , the master osc as phase argument in the synced (slave)sine would restart it ,but I guess that not the same as actual sync

The link to the carbon 111 page is exactly what I am doing in reaktor , same as fake resonance in casio cz synths .
It’s too much of a hassle to achieve this in supercollider

Um… What?

Here is the algorithm from that page, in SC code. (Which I posted earlier.)

(
{
	var sync = LFTri.ar(250);
	var phase = Sweep.ar(sync, SinOsc.kr(0.1).exprange(280, 2000));
	var synced = SinOsc.ar(0, (phase % 1) * 2pi);
	dup(LeakDC.ar(synced * sync) * 0.1)
}.play;
)

There’s like 4 lines of UGens.

I’m at some pains to imagine how only 4 lines of UGens qualify as a “hassle.” (I thought it was a cool technique and a cool way to implement it.)

In any case, if the help is not appreciated, then I’ll just stick to my own work next time.

hjh

With too much hassle I meant the use of a new object Sweep 'to achieve basic sync , that’s all .
Let’s be honoust , an included sync argument would be great

Your help is greatly appreciated

Given the suggestions of Scott and James, a solution would be easy. Clearly, as you just started with SC, this is not obvious and nearby: a pseudo ugen class fitting your needs. The definition would be really short, here’s a quick variant:

RingSync : UGen {
	*ar { |in, freq|
		var phase = Sweep.ar(in, freq);
		var synced = SinOsc.ar(0, (phase % 1) * 2pi);
		^synced * in
	}
}

Save as .sc file in the Extensions folder, restart SC and then the bespoken example turns into a one-liner:

{ RingSync.ar(LFTri.ar(250), 300) * 0.1 }.play

{ RingSync.ar(LFTri.ar(250), [300, 1000]) * 0.1 }.play

Further variants: choice of synced waveform per integer argument or by passed buffer(s), differing modulation algebra, leaking option etc.

BTW you can also search the mailing lists for “hard sync”:

For sc-users it gives 19 hits, including historically interesting ones:

https://www.listarc.bham.ac.uk/lists/sc-users-2004/msg07779.html

1 Like

Hm… It occurs to me that we might be seeing a difference between design ideals in software vs in synth modules.

In software, one design ideal is modularity: components with a clear focus. Also, if an input exists, then it has to be handled, and handling the input always takes CPU cycles (and realtime processing is time sensitive). So the sweet spot for computer UGen design IMO favors restricting inputs to the minimum necessary, and building more complex behaviors from combinations of units. From that perspective, driving a SinOsc by an explicit phase signal is an idiomatic approach. (And, as Daniel did, building an object that automates this construction is completely legitimate and thoroughly in keeping with the ideal of modularity.)

In hardware synth modules, there’s no CPU constraint to be shared across all modules, and designs are not malleable. So the incentive is to have enough inputs to make the module flexible – the sweet spot is more complex than a software unit generator. (I mention this because it seems that Reaktor is influenced by this approach, at least in terms of reducing the number of distinct modules and giving each module more features. That’s a legitimate design choice, especially for graphical environments, but a text-based computer language may not benefit from the same approach.)

In SinOsc, it would be silly not to provide a frequency input, and a phase input is useful for kick drum phase offsets and PM synthesis, so the gain in functionality vs the CPU cost is high. A sync input is likely to be used in a much smaller minority of cases, so the benefit:cost ratio is lower. That is: some small percentage of SinOsc usages would need sync, but every instance would have to handle the input (and be a bit slower for that). This would break a principle of “don’t do extra work” (especially in time sensitive applications). (It would be possible to optimize the case of a scalar sync input, but the initialization cost would still not be 0, and the maintenance cost for the all-volunteer dev team would increase by needing to manage more calculation functions.)

So I think SC strikes a legitimate balance between functionality and performance: common cases are streamlined, and rarer cases are possible to implement without much pain (and even that pain can be eliminated by creating pseudo-UGen classes, as Daniel demonstrated).

hjh

PS Edit: FWIW I just found that there’s no audio-rate resettable phasor in Pure Data at all (phase reset is by control message only), so what is slightly annoying in SC seems to be exceptionally difficult / maybe impossible in another notable FLOSS audio programming environment…

2 Likes

For some context: Here’s what it takes in Pd. It’s possible, just conceptually fairly advanced.

You thought Sweep was an injustice… in Pd you have to roll your own LFTri (the chain at the right, phasor~ + 0.75 mod 1.0 - 0.5, take absolute value, then * 4 - 1… that’s 7 objects for SC’s one LFTri), and roll your own resettable phasor (using rpole~ as an integrator, syncing it by passing 0 into both inputs for one sample only, and modding that to 0.0-1.0 – oh, and Pd-vanilla doesn’t have a signal rate mod operator. You have to use expr~, about which they always say “don’t use it because it’s too slow”… how about making sure the core operator set is complete then :face_with_raised_eyebrow: ).

(Edit: For posterity, I found after the fact that Pd has [wrap~], which would replace the fmod($v1, 1) expressions.)

3 minutes to write in SC, took me the better part of an hour to think this through in Pd.

pd-windowed-sync

hjh

Here’s a slightly amended version of Scott’s code that is maybe a bit closer to the target sound.
I made the following changes:

  • changed the curve of the pitch envelope to \squared
  • changed the envelope decay times to shorter times
  • changed the line "sig = (sig1 * sig2)**2; " to “sig = (sig1 * sig2).squared;” - I’m not sure technically why this sounds different (perhaps someone with deeper knowledge can explain)
  • adding a OnePole filter since there was one shown in the Reaktor patch.

In the code below, I used a SynthDef so it works with a pattern:

(
SynthDef(\fakeRes, {
	arg  out = 0, freq = 200, peamount = 900,
	pitchAtt = 0.001, pitchDec = 0.13,
	ampAtt = 0.03, ampDec = 1.0, level = 0.5;

	var sig, sig1, sig2, ampenv, pitchenv, sig2Phase;

	pitchenv = EnvGen.ar(
		// Env([0, 1, 0], [pitchAtt, pitchDec], [0, 0]),  // original
		Env([0, 1, 0], [pitchAtt, pitchDec],  \squared),
		// Env([0, 1, 0], [pitchAtt, pitchDec],  \cubed),
		// doneAction:2      // removed
	);

	ampenv = EnvGen.ar(
		Env([0, 1, 0], [ampAtt, ampDec], [0, 0]),
		levelScale: level,
		doneAction:2,
	);

	sig1 = SinOsc.ar(
		freq,
		mul:-0.53,
		add:0.5
	);

	sig2Phase = Sweep.ar(sig1, (freq + (pitchenv * peamount)));

	sig2 = SinOsc.ar(
		freq + (pitchenv * peamount),
		phase: 2pi * sig2Phase
	);

	// sig = (sig1 * sig2)**2;  // orig
	// sig = (sig1 * sig2).pow(2);  // same sound as orig
	sig = (sig1 * sig2).squared;  // sounds different

	sig = OnePole.ar(sig, -0.22); // added
	sig = sig * ampenv ! 2;
	Out.ar(0, sig);
}).add;
)

// play note
Synth(\fakeRes);  

// play pattern
(
~notePatt = Pbind(*[
	\instrument: \fakeRes,
	\degree, Pxrand([0, 2, 4, 8, 9, 10, 12, 16], 32),
	\scale, Scale.minor,
	\octave, 3,
	\peamount: Pwhite(200, 900, 32),
	\pitchAtt: 0.001,
	\pitchDec: Pwhite(0.3, 0.7, 32),
	\ampAtt: 0.001,
	\ampDec: Pwhite(0.5, 2, 32),
	\level: Pseq([0.9, 0.7, 0.6, 0.9, 0.5, 0.6, 0.9, 0.6, 0.8, 1, ], 2),
	\legato: 0.8,
	\dur: Prand([0.5, 0.75, 1], 32),
]);
p = ~notePatt.play;
)

p.stop;
3 Likes

I made a small mistake in the reaktor patch , it’s no use to square both the master and slave , squaring the slave is enough .
The filter at the end is actually a 1p high pass filter set to roughly 5 hz for dc offset elimination ( if anny )

pow and ** are defined for signals to avoid imaginary numbers when raising a negative number to a fractional power. I think it’s like pow(abs(x), exponent) * sign(x).

http://doc.sccode.org/Overviews/Operators.html#.pow

hjh

i was trying to use the example with passed envelopes. But when i change the value for \syncDcy of the \freqEnv inside the Pbind it wont change. whats wrong here? when i adjust the \atk of the \gainEnv its working fine. thanks :slight_smile:

 (
    SynthDef(\windowsync, {
    	arg out=0, pan=0, freq=440, amp=0.1,
    	syncEgTop=8, syncRatio=2, syncDcy=0.5;
    	
    	var gainEnv = \gainEnv.kr(Env.newClear(8).asArray);
    	var freqEnv = \freqEnv.kr(Env.newClear(8).asArray);
    	//var freqEnv = EnvGen.kr(Env([syncEgTop / syncRatio, 1], [syncDcy], \exp));
    	
    	var sig, fundamental, syncFreq, syncPhase;
    	
    	//frequency Envelope
    	freqEnv = EnvGen.kr(freqEnv);
    	
    	fundamental = LFTri.ar(freq);
    	syncFreq = freq * syncRatio * freqEnv;
    	syncPhase = Phasor.ar(fundamental, syncFreq * SampleDur.ir, 0, 1, 0);
    	
    	sig = SinOsc.ar(0, syncPhase * 2pi) * fundamental;
    	
    	// amp envelope
    	gainEnv = EnvGen.kr(gainEnv, doneAction:2);

    	sig = sig * gainEnv;
    	
    	sig = Pan2.ar(sig, pan, amp);
    	Out.ar(out, sig);
    }).add;
    )

    (
    Pdef(\windowsync,
    	Pbind(
    		\type, \hasEnv,
    		\instrument, \windowsync,
    				
    		\dur, 1,
    		\legato, 0.80,

    		\atk, 0.01,
    		\sus, (1 - Pkey(\atk)) * Pexprand(0.55,0.85,inf),

    		\gainEnv, Pfunc{|e|
    			var rel = (1 - e.atk - e.sus);
    			var c1 = exprand(2,6);
    			var c2 = exprand(-2,-6);
    			Env([0,1,1,0],[e.atk, e.sus, rel],[c1,0,c2])
    			//Env.adsr(0.01, 0.3, 0.6, 0.1)
    		},

    		\syncEgTop, 20,
    		\syncRatio, 2,
    		\syncDcy, 0.15,

    		\freqEnv, Pfunc{|e|
    			Env([e.syncEgTop / e.syncRatio, 1], [e.syncDcy], \exp)
    		},

    		\midinote, 36,
    		
    		\amp, 0.5,
    		
    		\out, 0,
    		\finish, ~utils[\hasEnv]
    	)
    ).play;
    )

// create a new event type called hasEnv
// which checks every parameter whose key ends in Env or env:
// - convert non-env values to envs (e.g 0 becomes Env([0,0],[dur]))
// - stretch envelope to last for the event's sustain (converted from beats to seconds)
~utils = ();
~utils.hasEnv = {
    // calc this event's duration in seconds
    var durSeconds = ~dur * ~legato / thisThread.clock.tempo;
    // find all parameters ending in env or Env
    var envKeys = currentEnvironment.keys.select{|k|"[eE]nv$".matchRegexp(k.asString)};
    envKeys.do{|param|
        var value = currentEnvironment[param];
        if (value.isArray.not) { value = [value] };
        value = value.collect {|v|
            // pass rests along...
            if (v.isRest) { v } {
                // convert non-env values to a continuous, fixed value env
                if (v.isKindOf(Env).not) { v = Env([v, v], [1]) }
            };
            // stretch env's duration
            v.duration = durSeconds;
        };
        currentEnvironment[param] = value;
    };
};

Event.addParentType(\hasEnv,(
    finish: ~utils[\hasEnv]
));

Your utility function adjusts the envelope duration to match the event duration.

If the envelope has only one segment, then that single segment’s duration must = the event sustain time.

FWIW I usually handle this by defining a timeScale parameter in the synth, and passing your durSeconds to it. Then I can pass an Env with duration 1 to match the event duration. Env duration 0.5 is half the event sustain, etc. I feel this is a little more flexible than overwriting every Env’s duration.

(The idea of converting numbers to Envs automatically is clever – I might steal that :grin: )

hjh

thanks ive used your approach from another thread and its working fine :slight_smile:

//frequency Envelope
freqEnv = EnvGen.kr(freqEnv, timeScale:time);

\syncDcy, 0.35,
\time, Pfunc { |ev| ev.use { ~syncDcy.value } / thisThread.clock.tempo }.trace,

the credits go to @elgiano for the clever idea :wink:

i was also playing around with GrainSin for achieving something similiar, is this correct?

(
h = Signal.hanningWindow(1024);
e = Buffer.loadCollection(s, h);

SynthDef(\windowsync, {
	arg out=0, pan=0, freq=440, amp=0.1,
	syncEgTop=8, syncRatio=2, syncDcy=0.5, shapeAmount=0.4, time=1,

	overlap=0.5, envBuf=0;

	var gainEnv = \gainEnv.kr(Env.newClear(8).asArray);
	var freqEnv = \freqEnv.kr(Env.newClear(8).asArray);

	var sig, fundamental, synced;
	var k = 2 * shapeAmount / (1 - shapeAmount);

	//frequency Envelope
	freqEnv = EnvGen.kr(freqEnv, timeScale:time);

	fundamental = GrainSin.ar(
		numChannels: 1,
		trigger: Impulse.ar(freq),
		dur: 1 / freq,
		freq: freq,
        envbufnum: envBuf
	);

	synced = GrainSin.ar(
		numChannels: 1,
		trigger: fundamental,
		dur: overlap / freq,
		freq: freq * syncRatio * freqEnv,
        envbufnum: envBuf
	);

	sig = synced.squared * fundamental;
	sig = LeakDC.ar(sig);

	// amp envelope
	gainEnv = EnvGen.kr(gainEnv, doneAction:2);
	sig = sig * gainEnv;

	// waveshaper
	sig = ((1 + k) * sig / (1 + (k * sig.abs)));

	sig = Pan2.ar(sig, pan, amp);
	sig = Limiter.ar(sig);
	Out.ar(out, sig);
}).add;
)

(
Pdef(\windowsync,
	Pbind(
		\type, \hasEnv,
		\instrument, \windowsync,

		\shapeAmount, 0.4,

		\dur, 1,

		\legato, 0.8,

		\atk, 0.01,
		\sus, (1 - Pkey(\atk)) * Pexprand(0.35,0.55,inf),

		\gainEnv, Pfunc{|e|
			var rel = (1 - e.atk - e.sus);
			var c1 = exprand(2,6);
			var c2 = exprand(-2,-6);
			Env([0,1,1,0],[e.atk, e.sus, rel],[c1,0,c2])
		},

		\syncEgTop, 75,
		\syncRatio, 2,
		\syncDcy, 0.35,
		\time, Pfunc { |ev| ev.use { ~syncDcy.value } / thisThread.clock.tempo }.trace,

		\freqEnv, Pfunc{|e|
			Env([e.syncEgTop / e.syncRatio, 1], [e.syncDcy], \exp)
		},

		\midinote, 36,

		\envBuf, e.bufnum,
		\overlap, 0.9,

		\amp, 0.5,

		\out, 0,
		\finish, ~utils[\hasEnv]
	)
).play;
)