DualOscOS - A dual wavetable Oscillator with cross feedback PM

hey, i have released my first c++ ugen. A highly optimized dual wavetable oscillator with dynamic mipmapping and sinc interpolation for bandlimiting and the additional possibility for oversampling (like OscOS). The oscillators are cross modulating each other via PM with a tracking One-Pole filter in the feedback path (via the pmFltRatio you get different PM feedback flavours). You can download the release here. If you want to test it you can use that code snippet here which creates a quick gui (make sure you have NodeProxyGui2). I hope the cross platform builds have been successful :slight_smile:
thanks @Sam_Pluta for the inspiration from OscOS.
Im very happy right now, that im currently extending my possibilities to contribute to this community, watch out for more :slight_smile:

// create a wavetable (or use your own)
(
t = Signal.sineFill(2048, [1], [0]);
u = Signal.sineFill(2048, 1.0/((1..512)**2)*([1,0,-1,0]!128).flatten);
w = Signal.sineFill(2048, 1.0/(1..512)*([1,0]!256).flatten);
x = Signal.sineFill(2048, 1.0/(1..512));
v = t.addAll(u).addAll(w).addAll(x);

b = Buffer.loadCollection(s, v);
)

(
// scale modulation depth of modulators between 0 and 1
var modScaleBipolarUp = { |modulator, value, amount|
	value + (modulator * (1 - value) * amount);
};

SynthDef(\dualOscOS, {
	
	var param, calcWavetableData;
	var wavetableDataA, wavetableDataB;
	var sigs, sig;
	
	// Global parameter function
	param = { |chainID, name, default, spec|
		var paramName = "%_%".format(chainID, name).asSymbol;
		NamedControl.kr(paramName, default, lags: 0.02, fixedLag: true, spec: spec);
	};
	
	// Function to calculate wavetable data
	calcWavetableData = { |chainID, phaseOffset|
		
		var tableIndexMF, tableIndexMod, tableIndex;
		var wavetable, sizeOfTable, pmIndex, pmFltRatio;
		var freq, phase;
		
		freq = param.(chainID, \freq, 440, spec: ControlSpec(1, 1000, \exp));
		phase = Phasor.ar(DC.ar(0), freq * SampleDur.ir);
		
		/////////////////////////////////////////////////////////////////////////////////
		
		// Create table index modulation
		tableIndexMF = param.(chainID, \tableIndexMF, 0.1, ControlSpec(0.1, 1));
		tableIndexMod = { |phase|
			SinOsc.ar(tableIndexMF, phase + phaseOffset * pi)
		};
		
		tableIndex = modScaleBipolarUp.(
			modulator: tableIndexMod.(0.5),
			value: param.(chainID, \tableIndex, 0, ControlSpec(0, 1)),
			amount: param.(chainID, \tableIndexMD, 1, ControlSpec(0, 1))
		);
		
		// table params
		wavetable = param.(chainID, \sndBuf, 0);
		sizeOfTable = BufFrames.kr(wavetable) / 2048;
		
		// Phase modulation params
		pmIndex = param.(chainID, \pmIndex, 0, ControlSpec(0, 5));
		pmFltRatio = param.(chainID, \pmFltRatio, 1, ControlSpec(1, 5));
		
		/////////////////////////////////////////////////////////////////////////////////
		
		(
			phase: phase,
			pmIndex: pmIndex,
			pmFltRatio: pmFltRatio,
			wavetable: wavetable,
			sizeOfTable: sizeOfTable,
			tableIndex: tableIndex
		);
		
	};
	
	wavetableDataA = calcWavetableData.(\one, 0);
	wavetableDataB = calcWavetableData.(\two, 1);
	
	sigs = DualOscOS.ar(

		bufnumA: wavetableDataA[\wavetable],
		phaseA: wavetableDataA[\phase],
		numCyclesA: wavetableDataA[\sizeOfTable],
		cyclePosA: wavetableDataA[\tableIndex],

		bufnumB: wavetableDataB[\wavetable],
		phaseB: wavetableDataB[\phase],
		numCyclesB: wavetableDataB[\sizeOfTable],
		cyclePosB: wavetableDataB[\tableIndex],

		pmIndexA: wavetableDataA[\pmIndex],
		pmIndexB: wavetableDataB[\pmIndex],
		pmFilterRatioA: wavetableDataA[\pmFltRatio],
		pmFilterRatioB: wavetableDataB[\pmFltRatio],

		oversample: 0
	);

	sig = XFade2.ar(sigs[0], sigs[1], param.(\all, \chainMix, 0.5, ControlSpec(0, 1)) * 2 - 1);

	sig = sig * param.(\all, \amp, -25, ControlSpec(-35, -5)).dbamp;
	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

	sig = LeakDC.ar(sig);
	sig = Limiter.ar(sig);
	Out.ar(\out.kr(0), sig);
}).add;
)

(
~nodeProxyGui2 = { |synthDefName, initArgs=#[], excludeParams=#[], ignoreParams=#[]|

	var synthDef, nodeProxy;

	synthDef = try{ SynthDescLib.global[synthDefName].def } ?? {
		Error("SynthDef '%' not found".format(synthDefName)).throw
	};

	nodeProxy = NodeProxy.audio(s, 2);
	nodeProxy.prime(synthDef).set(*initArgs);

	NodeProxyGui2(nodeProxy, show: true)
	.excludeParams_(excludeParams)
	.ignoreParams_(ignoreParams)
};
)

(
g = ~nodeProxyGui2.(\dualOscOS,
	[\one_sndBuf, b.bufnum, \two_sndBuf, b.bufnum],
	[\one_sndBuf, b.bufnum, \two_sndBuf, b.bufnum],
	[\all_amp]
);
)

g.randomize;
g.vary;
13 Likes

wow, this is wild, cool sounds and noise from your example, thank you for sharing.

i got stuck for longer than i’d like to admit getting it to run, on linux the DualOscOS.scx needs to be renamed to DualOscOS.so, just in case someone else gets the exception in GraphDef_Recv: UGen ā€˜DualOscOS’ not installed error.

1 Like

Sounds great) Thank you!

1 Like

hey,

I have just added SuperSawOS(i will add more sophisticated anti-aliasing soon)

(
{
	var sig = SuperSawOS.ar(
		freq: 220,
		mix: 1.0,
		detune: 0.75,
		oversample: 2
	);
	sig!2 * 0.1;
}.play;
)
1 Like

Lovely. Is it based on this one? I really like this SynthDef and I’ve got some sequences I imagine I’d do with jp-8080 (excluding effects)


2 Likes

Yes, based on that :slight_smile:
Lovely stuff https://youtu.be/tb0G3ax8VgQ?si=0VrKUTTWYnkUNU8M

1 Like
2 Likes

For me it was especially after this Richard Sides - don't blow it in the vector

2 Likes

The First one is so cool !

Hi! I see that there’s also a SingleOscOS. I wonder what the difference is between SingleOscOS and OscOS? They both have equal efficiency but sound completely different in extreme modulation tests (done with UnitShapers)

hey, thats just my personal implementation. You can just use OscOS.

They both sound nice in different sitatuations and bring their own character. That’s cool

Just updated the helpfile to make that clear :slight_smile:
What Kind of Unitshapers have you used, would like to hear some sounds :slight_smile:

1 Like

SCurve and JCurve (and various windows)

b = Buffer.loadCollection(s, Signal.sineFill(2048, [1]))

(
{ 
	var phase = Phasor.ar(DC.ar(0), 50 / SampleRate.ir, 0, 1);
	var trajectory = SCurve.ar(phase, shape: 0.9, inflection: 0.5) * Line.ar(0, 1000, 10, doneAction: 2);
	var sig = SingleOscOS.ar(b, phase * trajectory, oversample: 4);
	sig ! 2
}.play
)

(
{
	var phase = Phasor.ar(DC.ar(0), 50 / SampleRate.ir, 0, 1);
	var trajectory = SCurve.ar(phase, shape: 0.9, inflection: 0.5) * Line.ar(0, 1000, 10, doneAction: 2);
	var sig = OscOS.ar(b, phase * trajectory, oversample: 4);	
	sig ! 2
}.play
)

While in a granular context its not obvious which one to use. In different parameter sets I’ll likely choose different oscillators from these two. Both are super in its own cases

SingleOscOS

OscOS

hey, cool :slight_smile:

The Line Ugen here scales the number of cycles. Thats not the same as having a frequency going up from lets say 50hz to 5000 hz. Thats something like ā€œphase scalingā€, which is not really a term (at least none that i could find in my research). Your phases arent between 0 and 1 anymore if you dont wrap them between 0 and 1 (i guess both ugens do the wrapping between 0 and 1 internally).
Multiplying phases by time varying signals can cause some unpredictable behaviour. But if it sounds cool its fine :slight_smile: Thats something in between phase modulation (you would add your modulation term to the linear phase) and phase shaping (where you take your linear phase and put it into a non-linear transfer function). Ive tried to address that with the pulsar example in the guide on voice allocation, where we latch the number of cycles per grain.

1 Like

I have merged the DualOscOS repository with my GrainUtils repository Releases Ā· dietcv/GrainUtils Ā· GitHub. From now on you can find these phasor driven oscillators and probably some more in future in my GrainUtils repository. I will therefore delete the DualOscOS repository.
The SuperSawOS is not part of the GrainUtils repo for two reason:

  • its not phasor based, GrainUtils is a library about phasor based processing mainly for granulation
  • there are better ways for anti-aliasing for simple waveforms like saw waves than using oversampling (i have saved the current approach and will work on it together with my hard sync attempt in the future).
2 Likes