Fake resonance , aka windowed sync

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;
)

For a bit of thread resurrection, as I have been evaluating Reaktor myself recently (FYI: you have to pay for this or find and employer that does, there’s no eval license for Reaktor core)… it’s also the case that Reaktor (core) has both hard sync and non-sync “macros” for the basic oscillators. The latter are called “‘Slave’ Oscillators” in the Reaktor core, see e.g. p. 129 in the manual. I suppose the slight downside in SC is that there’s not a core SC class library that does that, but as noted above it’s not hard to write one.

Also a couple of points I’d like to make here about Reaktor core: it uses some kind of (jit?) compiler, but the documentation on how feedback loops and mergers are handled seems incredibly obscure to me, even though there’s a fair attempt to document it in the aforementioned manual (pp. 100-120). To give some examples of huh moments:

Often in Structure building there is a question of whether a Latch should be used at a particular position. In answering this question the logical function of the Latch and the associated CPU consumption can be considered. As it was already mentioned, the compiler is treating latches (and the ‘read followed by a write’ pattern in general) in a special optimized way. Thus, the relationship between the usage of a Latch at a particular place and the associated change in the CPU load is not straightforward.
• Generally, if it is not necessary to store the value into the memory (because the value is
immediately read afterwards anyway), the compiler will not do so. In such situations a
Latch by itself will not add to the CPU load.
• On the other hand, not placing a Latch on some signal path may result in a more complicated triggering logic of the downstream Structure and thus in a higher CPU load produced not by the Latch itself, but by this downstream Structure.Thus, there is no general rule whether using a Latch will increase the CPU load or decrease it.
It is best to simply use latches wherever logically appropriate.

As Modulation Macros are simply shortcuts for the mathematical operations combined with Latches, the same applies to the Modulation Macros.

Or

At each mergepoint the compiler attempts to detect whether a splitting endpoint occurs. In cases where ‘chaotic routing’ (when the routing branches are not merged back together, but rather some signals with unrelated triggering sources are mixed) has been used in excessive amounts, the analysis time can grow drastically. In the worst cases this can cause the compiler to appear to ‘hang indefinitely’ (the compiler’s progress bar stops completely).

NI is aware of this issue and is looking for a solution.

In a ‘chaotic merging’ situation practically each Module represents a new mergepoint with a new set of triggering conditions. While the analysis of these triggering conditions consumes the compilation time, the respective runtime check eventually generated by the compiler consumes the runtime.

I think few other than the Zavalishin guy that NI hired to write it really understand what that means. To me, it seem the compiler attempts to do block processing by merging branches, somewhat similar to how some GPU-based audio is done, although Reaktor does it only on the CPU.

Also, the Reaktor compiler does a modicum of peephole optimization like replacing division with multiplication where appropriate (p. 123). The somewhat more clear part(s) are that some feedback loops are automatically handled in Reaktor core by the compiler inserting a one sample delay; this is in contrast with Faust where you have to be explicit about those.

But, in general, I have not been impressed with the real-world performance of some example instruments like the nGrano sampler (which is also not free, by the way). It’s much slower than most other granulators I’ve seen implemented in other systems, including gen-based stuff in Max 8. Pretty much the same goes for NI’s founder’s pet project, the Kontour synth, which is some kind of mega-FM synth with waveshapers and lots of feedback paths. (To their credit though, the NI synths based on Reaktor tend to have good documentation in terms of block diagrams.)

Faust probably blows Reaktor core away in terms of optimizations and performance. But it’s hard to find true comparative benchmarks due to the walled-gardened nature of the Reaktor ecosystem. The Reaktor fanboys also love to rub it your face that it has been used on “multiple platinum-award works” and the like (and ask you if Max/gen, SC or Faust have won the same), so real technical discussions of the Reaktor compiler performance etc. tend to be rather nonexistent on the NI-focused forums.


To come back to the actual topic at hand here, using a BufRd with a Phasor make this easy enough and also works with arbitrary “wavetables”, without having to actually use that special SC format. I’m honestly not sure if there’s any point in doing it with a Sweep and a SinOsc, since the latter is also table-based internally, other than for typing a bit less. The FSinOsc might be a different matter, but I recall seeing some discussions that it’s not actually faster than the table-based SinOsc. I’m not too surprised since modern processor have decent cache memory including at L1 (64Kb on Intel ones of the past decade), so unless you exceed that you’re probably not going to see much of an issue. SinOsc seems to use 8K tables by default.