Oversampling oscillators

Thats awesome. I Would Love to help, but my c++ is very Limited i can only compare it to my sc implementation but don’t know where i would find all the necessary Bits and pieces in the cpp and hpp files. Maybe we could go through every step of the sc implementation and think why it works and how it has to be implemented across platforms.

is that the foldover of the harmonic that you mean? I think the sinc mip mapping is implemented correctly but the aliasing is coming from the phase itself.

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

(
{
	var phase = Phasor.ar(0, 440 * SampleDur.ir);
	var sig = OscOS.ar(b,
		phase,
		buf_divs: 1, buf_loc: 0, oversample: 0
	);
	sig!2 * 0.1;
}.play;
)

s.freqscope;

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

(
{
    var rate, phase, sig;

	rate = SinOsc.ar(0.1, 1.5pi).linlin(-1, 1, 100, 8000);
    phase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);
	sig = OscOS.ar(~sndBuf, phase, 1, 0, 0);
	//sig = BufRd.ar(1, ~sndBuf, phase * BufFrames.kr(~sndBuf), 1, 4);

    sig = LeakDC.ar(sig);
    sig!2 * 0.1;
}.play;
)

with the Sine wave, you really only see it up around 8K, but that is right where the mipmap should be making a sine wave in all cases:

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

(
{
	var phase = Phasor.ar(0, MouseX.kr(100,16000).poll * SampleDur.ir);
	var sig = OscOS.ar(b,
		phase,
		buf_divs: 1, buf_loc: 0, oversample: 0
	);
	sig!2 * 0.1;
}.play;
)

It is a third harmonic distortion. Frequency is 3x the fundamental.

you can hear it maybe at the root frequency of this:

p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
b = Buffer.read(s, p);
(
    {
	var fund_of_buf = 1/BufDur.kr(b);
        var phase = LFSaw.ar(MouseY.kr(fund_of_buf/8,fund_of_buf*(2**10.5), 1).poll).range(0,1);
        var osc = OscOS.ar(b,phase,1,MouseX.kr,0);
        osc.dup*0.2
    }.scope
)

But it is pretty cool to be able to shift a 4 second sound file up 10 octaves without foldover.

I don’t know. All these things have trade-offs. If there is anything I have learned from making these libraries is that digital oscillators, like analog oscillators, have character, and that should be embraced.

Sam

hey,

Im more then thankful for all the work you have been putting in that and im very frustrated that i cant offer additional help, but i think a frequency sweep with a sine wave up to 8000 hz should not cause any aliasing. The last time imo aliasing has had any character was in y2k.

lol. well, then the whole point of this library is to get this programming language out of the 90s.

but i’ll figure it out. i’m doing something dumb. i just need to run the symposium now.

sam

1 Like

This is a heavy burden to carry for sure. I have devoted countless hours the last years to try to make a contribution to that on the synthesis side of things and im more then happy if we could figure out what could be done next.

hi @Sam_Pluta,

i just tried the new OscOS package and played a longer sound file with more harmonic material with the oscillators.
there seems to be some strong artifacts/aliasing, which is quite clear when compared to a simple playbuf. heres the example and the sound i used.

b = Buffer.read(s,"/Users/jan/Sound/Recordings/harmsnd.aif");

(
    {
	//var osc = OscOS.ar(b,Phasor.ar(0,BufRateScale.kr(b)*0.7,0,BufFrames.kr(b))/BufFrames.kr(b),1,0,1);
	var osc = OscOS3.ar(b,-1,0.7/BufDur.kr(b),0,1,0,1,1,1,0,0,1);  //either of the OscOS has artifacts
	osc
    }.play
)

//compare to

{PlayBuf.ar(1,b,0.7,1,0,1)*1}.play

harmsnd.aif (1.5 MB)

maybe this is the same issue that is already pointed out but i thought id just report this for the use case of a PlayBuf alternative.

Thanks!

thank you for this. that is helpful.

it is not a replacement for PlayBuf, but that is the distortion. Knowing it is in both really helps.

Ack.

Sam

Couple odd things.

  • In OscOS, shouldn’t buf_loc have a default = 0? OscOS.ar(b, phase) → ERROR: K2A arg: ‘in’ has bad input: nil
  • Should OscOS guard against the buffer being freed before the synth is fully gone?
(
s.waitForBoot {
	b = Buffer.alloc(s, 2048, 1, completionMessage: { |buf|
		// btw I see everyone is doing loadCollection for this
		// but you can turn off wavetable format in the b_gen message
		buf.sine1Msg([1], asWavetable: false);
	});
	s.sync;
	a = {
		var freq = LFDNoise3.kr(8).exprange(200, 800);
		var phase = Phasor.ar(0, freq * SampleDur.ir, 0, 1);
		(OscOS.ar(b, phase, buf_loc: 0) * 0.1).dup
	}.play;
};
)

// later
b.free;

--> Server 'localhost' exited with exit code 0.

Of course it’s wrong to run a wavetable oscillator without a wavetable, but silence would be a better outcome than kaboom, your set died onstage.

hjh

Thanks for those. Will implement.

Sam

OK. All these things should be fixed.

Notes:

  1. OscOS now operates differently when it uses oversampling and when it doesn’t. In non-OS mode, it removes the top octave of harmonics (like @dietcv’s SC example). It should be no different form dietcv’s example, except when the fundamental frequency is in the top octave. In oversampling mode, it puts the top octave back into the signal and removes the foldover with oversampling.

In the future, I would like to use ffts for the sinc interpolation and use a bigger sinc function. This is not happening any time soon though.

  1. OscOS3 uses quadratic interpolation only.
  2. fixed the crash when buffer disappears issue for all wavetable UGens.
  3. Shaper, ShaperOS, and SergeFoldOS now all use quadratic interpolation
  4. OscOS only uses a 8 point sinc interpolation, so it is great on short files, but won’t replace PlayBuf. The distortion we are experiencing on long files is because of this, but it is not an issue with shorter files. Here is a nice example, though, to get around that:
p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
b = Buffer.read(s, p);
(
    {
    var fund_of_buf = 1/BufDur.kr(b);
        var freq = MouseY.kr(fund_of_buf/8,fund_of_buf*(2**10.5), 1);
        var phase = LFSaw.ar(freq).range(0,1);
        
        var osc = OscOS.ar(b,phase,1,MouseX.kr,0);
        var br = BufRd.ar(1, b, phase*BufFrames.kr(b), 1, 4);

        //will crossfade with 
        var cross = LinXFade2.ar(br, osc, (freq.abs-1).clip(0,400).linlin(0,8,-1,1).poll);

        cross = HPF.ar(cross, 20);
        
        cross.dup*0.2
    }.scope
)

Sam

3 Likes

Is there a possibility to set the default value for the Buchla and Serge Wavefolder normalized to 1?
I think it would be desirable that a value of a zero waveshaping amount would mean no waveshaping but with full amplitude of the dry signal.

So, with an amplitude of up to 1 there is no distortion and then distortion starts when the signal is above/below 1/-1? Think that makes sense.

Yes, exactly. I think when adding distortion you don’t want to Attenuate the Signal below 1/-1.

I have seen that you have updated the BuchlaFoldOS and the SergeFoldOS ugens with the latest update. Thats really cool :slight_smile:
I seems to me that BuchlaFoldOS is scaled correctly, while SergeFoldOS isnt.
What i would expect is that if amp = 1, we just get the dry signal at full amplitude and no distortion.

(
{
	var lfo = SinOsc.ar(1).linlin(-1, 1, 0, 1);
	var sig = SinOsc.ar(110);
	var buchla = BuchlaFoldOS.ar(sig, lfo.linlin(0, 1, 1, 10));
	var serge = SergeFoldOS.ar(sig, lfo.linlin(0, 1, 1, 10));
	[sig, buchla, serge];
}.plot(0.02);
)

I have additionally looked at the ShaperOS Ugen after i was getting some weird audio glitches when hitting cmd+period using the tanh function from the helpfile with ShaperOS.
Im not an expert on waveshaping but im not sure if we would expect that the waveform has a kink at its peak. Im additionally not sure about the correct scaling, i would expect that for amp = 1, we just get the dry signal at full amplitude and no distortion.

~tanh_buf = Env([2.neg, 2],[1]).asSignal(2048).tanh.normalize;
~tanh_buf.plot;
~tanh_buf = Buffer.loadCollection(s, ~tanh_buf);

(
{
	var sig = SinOsc.ar(110);
	var shaper = ShaperOS.ar(~tanh_buf, sig, oversample: 0);
	[sig, shaper];
}.plot(0.02);
)

I have also been modulating the amplitude of the input signal for BuchlaFoldOS and SergeFoldOS with an LFO instead of modulating the Ugens amplitude argument, which gives a different result. Wouldnt it be more direct to get rid of the amplitude argument in both Ugens and just modulate the amplitude of the signal, to get distortion when its above 1?

(
{
	var lfo = SinOsc.ar(1);
	var sig = SinOsc.ar(110) * lfo.linlin(-1, 1, 1, 10);
	var buchla = BuchlaFoldOS.ar(sig);
	var serge = SergeFoldOS.ar(sig);
	[sig, buchla, serge];
}.plot(0.02);
)

hey, Ive made an observation today: OscOS creates aliasing for low input rates:

(
~sndBuf = Buffer.loadCollection(s, Signal.sineFill(2048, [1]), 1);

{
	var freq = 1;
	var phase = Phasor.ar(DC.ar(0), freq * SampleDur.ir);
	var sig = OscOS.ar(
		bufnum: ~sndBuf,
		phase: phase,
		buf_divs: 1,
		buf_loc: 0,
		oversample: 0
	);
	sig!2 * 0.1;
}.play;
)

s.freqscope;

All replies in one and thank you for your comments:

  1. BuchlaFold and SergeFold will be as they are. I don’t remember why, but SergeFold can’t scale the way you want it for technical reasons.

  2. Kink in ShaperOS is a bug. Use ShaperOS2, if needed:

(
{
	var sig = SinOsc.ar(110);
	var shaper = ShaperOS2.ar(~tanh_buf, sig, 1, 0, oversample: 0);
	[sig, shaper];
}.plot(0.02);
)

I should probably simplify the library anyway.

  1. Amplitude Modulating the signal before and after the distortion is going to come up with different results. That is what you are comparing.

  2. The aliasing with OscOS is because the 8 point Sinc interpolation cannot perfectly represent a sine wave at that frequency. We would need more points for that to work. I banged my head against the wall on this for about a week before I figured out that this was the reason. A 16 point interpolation should suffice if someone wants to hack the UGen.

Sam

hey, just one quick reply:

what i dont understand is, why the initial sc example doesnt have this problem:

// prepare buffers
(
var getSinc = { |numRipples|
    var x = Array.interpolation(4096, -1.0, 1.0);
    sincPi(x * 0.5pi * numRipples);
};
var sinc = getSinc.(8);
var kaiser = Signal.kaiserWindow(4096);
var windowedSinc = sinc * kaiser;
~sincBuf = Buffer.loadCollection(s, windowedSinc);
)

~sndBuf = Buffer.loadCollection(s, Signal.sineFill(2048, [1]), 1);

// run example!
(
var rampToSlope = { |phase|
    var history = Delay1.ar(phase);
    var delta = (phase - history);
    delta.wrap(-0.5, 0.5);
};

var sincInterpolated = { |phase, sndBuf, sincBuf, sampleSpacing, numSamples = 8|

    var sampleIndex, fracPart, intPart;
    var samples, windows;
    var halfSamples = numSamples.div(2);

    // Calculate the base sample position
    sampleIndex = phase * BufFrames.kr(sndBuf) / sampleSpacing;
    fracPart = sampleIndex.wrap(0, 1);  // fractional part for interpolation
    intPart = sampleIndex - fracPart;   // integer part for base position

    // Get sample values at integer offsets
    samples = (halfSamples.neg..(halfSamples - 1)).collect{ |offset|
        var readPos = ((intPart + offset) * sampleSpacing).wrap(0, BufFrames.kr(sndBuf));
        BufRd.ar(1, sndBuf, readPos, interpolation: 1);
    };

    // Get window values from sinc function
    windows = (1..numSamples).collect{ |i|
        var sincPos = (i / numSamples) - (fracPart / numSamples) * BufFrames.kr(sincBuf);
        BufRd.ar(1, sincBuf, sincPos, interpolation: 4);
    };

    (samples * windows).sum;
};

var mipmapInterpolate = { |phase, sndBuf, sincBuf|

    var slope, samplesPerFrame, octave, layer;
    var spacing1, spacing2, sig1, sig2;

	// Calculate mipmap parameters
    slope = rampToSlope.(phase);
    samplesPerFrame = slope.abs * BufFrames.kr(sndBuf);
    octave = max(0, log2(samplesPerFrame));
    layer = octave.ceil;

	// Calculate spacings for adjacent mipmap levels
    spacing1 = 2 ** layer;
    spacing2 = 2 ** (layer + 1);

	// Get and crossfade interpolated signals
    sig1 = sincInterpolated.(phase, sndBuf, sincBuf, spacing1);
    sig2 = sincInterpolated.(phase, sndBuf, sincBuf, spacing2);
    LinXFade2.ar(sig1, sig2, octave.wrap(0, 1) * 2 - 1);
};

{
    var rate, phase, sig;
	rate = 1;
    phase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);
    sig = mipmapInterpolate.(phase, ~sndBuf, ~sincBuf);
    sig!2 * 0.1;
}.play;
)

s.freqscope;

i havent been modulating before and after but compared:
1.) amplitude modulation of the amp argument of the distortion unit
2.) amplitude modulation of the signal before it is plugged into the distortion unit

For now, it is what it is. When I was making this, I was showing this distortion on in the Ugen and in the language. I think this is a small price to pay for the benefits it otherwise brings.

The mul argument wraps the signal in a MulAdd Ugen and applies the multiplication after the UGen.

Sam