Oversampling oscillators

Comparing BufRd and OscOS, by modulating a harmonic tone in a buffer i get discontinuity from the latter. Is there a known reason why, or a possible way to avoid it?

(
    {
	var read = SinOsc.ar(8);
	var phase = (Phasor.ar(0,BufRateScale.kr(b)*read,0,BufFrames.kr(b))/BufFrames.kr(b));
	var osc = (OscOS.ar(b,phase,1,0,4,0.5));
	var osc2 = (BufRd.ar(1,b,phase*BufFrames.kr(b),1,4)*0.5);
	[osc,osc2]
}.play
)

edit: also audible by modulating the standard sample strongly pitched down:

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
    {
	var read = SinOsc.ar(4);
	var phase = (Phasor.ar(0,BufRateScale.kr(b)*read*0.01,0,BufFrames.kr(b))/BufFrames.kr(b));
	var osc = (OscOS.ar(b,phase,1,0,4,0.5));
	var osc2 = (BufRd.ar(1,b,phase*BufFrames.kr(b),1,4)*0.5);
	[osc,osc2]
}.play
)

You should look at your phasor. What you are hearing is the massive spike in the phasor when it jumps from 0 to 1 and back again. Theoretically that shouldn’t matter, so I need to check my code. But theoretically Phasor also shouldn’t be doing what it is doing either.

Try it with LFSaw instead. Are you getting the same distortion?

There is also a design decision I made and the code currently upsamples the phase input unless the change is >0.5. Not sure if that is the right move, but it could be easily disabled.

Sam

With LFSaw, and also a phase driven SawOS it seems to result in no distortion!
So in principle there’s no downsides to using OscOs as a substitute for BufRd? Or still there are limitations to be aware of (i imagine buffer size because designed for wavetable use?)

It is not designed for wavetable use, so it should work with any sized buffer. Let me know if it doesn’t.

With the current implementation, jumping around the buffer is going to result in distortion of some kind, though I don’t know if it would be worse than jumping around a buffer with BufRd. I think I could just get rid of the internal upsampling.

Sam

1 Like

This is great, ill post when there should be any issue arising, thanks so much for this gem!

Now a test case where a pure sine is run in OscOS. Trying to use SawOs for driving its phase, the spike again occurs, contrary to other non- aliased phasors.
(I’m assuming that running a non aliasing phasor would be consequential for preventing aliasing in OscOS?)

(
b = Buffer.alloc(s, 512, 1);
b.sine2([1.0], [1], asWavetable:false);
)


(
    {
	var freq = 27;
	var phase1 = SawOS.ar(freq,-1).linlin(-1,1,0,1);
	var phase2 = LFSaw.ar(freq.neg).linlin(-1,1,0,1);
	var phase3 = Phasor.ar(0,freq/s.sampleRate);
	var osc = (OscOS.ar(b,[phase1,phase2,phase3],1,1,1,1));
	osc
}.plot(1/20)
)

A phasor is a sawtooth. An anti-aliased sawtooth has Gibbs effect wiggling at the corners. If this is being used to drive an oscillator’s phase, the Gibbs effect wiggles will introduce distortion. If the goal is a pure sine, then this distortion is not desired.

So you do not want to use SawOS to produce the phase (which principle is also borne out in the results).

hjh

i see, thank you james, i wasn’t aware of the existence of such an effect!

AFAICS it’s jumping 0 to 1 to 0 because of floating-point rounding error in the integration of the SinOsc input. “Theoretically” the integral of one cycle of a sine wave should be 0, but we don’t have perfect precision, so the sum is going slightly below 0.

However, if the wavetable properly wraps around, the discontinuity disappears in the output:

b = Buffer.alloc(s, 2048, 1, { |buf| buf.sine1Msg([1], 1, 0, 1) });

(
p = {
	var read = SinOsc.ar(8);
	var frames = BufFrames.kr(b);
	var phase = Phasor.ar(0, BufRateScale.kr(b) * read, 0, frames);
	var osc = OscOS.ar(b, phase / frames, 1, 0, 4, 0.5);
	var osc2 = BufRd.ar(1, b, phase, 1, 4) * 0.5;
	[phase / frames, osc, osc2]
}.plot(0.2);
)

sc-wrapped-phasor

So the discontinuity would be exposed only if the wavetable is discontinuous when wrapping from end to beginning or vice versa – in which case it isn’t a true wavetable.

hjh

This is good to know! i was in fact trying to avoid making single wavetables by reading directly from large buffers. These then wouldn’t wrap around 0, but could be windowed with an envelope.

Follow up, practical question regarding wavetables in OscOs: as it reads from a single buffer is there a straightforward way to to concatenate/collect different buffer wavetables (e.g. a folder with many such wavetabes as audio files) into a single one?

Thanks,
Jan

To my knowledge, nobody has yet written a general-purpose wavetable manager because there wasn’t anything really good to play it. However array concatenation is a pretty basic operation so I don’t think anyone should take the lack of a “here let me do it for you” object as a serious obstacle.

Last year I wrote a quark about wavetables – it does have a WavetablePrep class which might get you part of the way. The main purpose of this class was mipmapping the waveforms as an attempt to reduce aliasing without an oversampling UGen – this feature wouldn’t be needed with OscOS. This class might get you most of the way there; maybe an example later today.

Sure. You can expect some extra high frequency sizzle from the discontinuity – can be a nice gritty sound.

hjh

Not yet… I’m running into a crashing bug.

@Sam_Pluta – tried building 64 wavetables of 2048 samples each. OscOS dies when bufloc is nonzero.

(
// 131072 frames, a11wlk01 has 188893 frames, OK
var size = 2048, numTables = 64;
var w = Signal(size * numTables);
var chunk;

var file = SoundFile.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");

if(file.notNil) {
	protect {
		numTables.do {
			chunk = Signal.newClear(size);
			file.readData(chunk);
			// remove DC and normalize
			chunk = (chunk - chunk.mean).normalize;
			w.addAll(chunk);
		};
		b = Buffer.sendCollection(s, w, 1, action: { "done".postln });
	} {
		file.close
	};
} {
	"File open failed".warn;
};
)

// wait until done

(
var numTables = 64;
a = {
	(OscOS.ar(
		b,
		Phasor.ar(0, MouseY.kr(200, 800, 1) * SampleDur.ir, 0, 1),
		numTables,
		MouseX.kr(0, 0.999 /*numTables - 1.001*/),
		4
	) * 0.1).dup
}.play;
)

gdb says (omitting some of the early frames, just normal stuff):

#0  0x00007fffefa58ecb in OscOS::OscOS::Perform(float const*, float, float, float, int, float) ()
   from /home/dlm/.local/share/SuperCollider/Extensions/OversamplingOscillators/BuchlaFoldOS.so
#1  0x00007fffefa59177 in OscOS::OscOS::next_os(float const*, float, float, float, int, float) ()
   from /home/dlm/.local/share/SuperCollider/Extensions/OversamplingOscillators/BuchlaFoldOS.so
#2  0x00007fffefa592f6 in OscOS::OscOS::next_aa(int) ()
   from /home/dlm/.local/share/SuperCollider/Extensions/OversamplingOscillators/BuchlaFoldOS.so
#3  0x00007fffefa59e1c in OscOS::OscOS::OscOS() ()
   from /home/dlm/.local/share/SuperCollider/Extensions/OversamplingOscillators/BuchlaFoldOS.so
#4  0x00005555555b4cf6 in Graph_FirstCalc (inGraph=0x7fff9f306700)
    at /home/dlm/share/superc/server/scsynth/SC_Graph.cpp:506
#5  0x00005555555bad5d in Group_Calc (inGroup=<optimized out>)
    at /home/dlm/share/superc/server/scsynth/SC_Group.cpp:69
#6  0x00005555555bad5d in Group_Calc (inGroup=<optimized out>)

If I replace the MouseX.kr with 0, it doesn’t crash. If I replace it with 1, it crashes. 63/64, crashes. 0.000001, no crash.

What is the unit of bufloc? No documentation and no comment in the class file, I’m stabbing in the dark here.

hjh

You can use FluidBufCompose from the FluCoMa plugins:

https://www.flucoma.org/

Sam

1 Like

I don’t get this error. I looked at the code and maybe maybe maybe it was me using a float when I should have been using an int. I know you are compiling from source. Could you try to download and compile again?

bufloc is 0-1. It indicates the location in the buffer array. With 64 buffers the divisions are at bufindex/numbufs-1. Added to the help file.

Sam

Now I see that within the last hour, you deleted the offending code :laughing: no phase interpolation · spluta/OversamplingOscillators@9a93a02 · GitHub

    float phase_diff = (phase1 - m_last_phase);
    float loc_diff = (buf_loc1 - m_last_buf_loc);

These need to be divided by the oversampling ratio(?)… trying it.

EDIT: AFAICS, this fixes it:

    float loc_diff = (buf_loc1 - m_last_buf_loc) / oversample.getOversamplingRatio();

BTW is it optimal to call oversample.getOversamplingRatio() repeatedly?

I can PR these cleanups if you like.

It sounds fantastically gross btw with these non-wavetable wavetables, love it!

(
// 131072 frames, a11wlk01 has 188893 frames, OK
var size = 2048, numTables = 64;
var w = Signal(size * numTables);
var chunk;

var file = SoundFile.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");

if(file.notNil) {
	protect {
		numTables.do {
			chunk = Signal.newClear(size);
			file.readData(chunk);
			// remove DC and normalize
			chunk = (chunk - chunk.mean).normalize;
			w.addAll(chunk);
		};
		b = Buffer.sendCollection(s, w, 1, action: { "done".postln });
	} {
		file.close
	};
} {
	"File open failed".warn;
};
)

// wait, then...

(
a = {
	(OscOS.ar(
		b,
		Phasor.ar(0, MouseY.kr(50, 200, 1) * SampleDur.ir, 0, 1),
		b.numFrames / 2048,  // numTables,
		MouseX.kr(0, 0.999),
		4
	) * 0.1).dup
}.play;
)

hjh

I just submitted a PR for a better idea.

hjh

OK. OscOS is much improved. If you want to use it, download the new release. I know I should be more like nathan and make sure my code works before release. Will do so in the future. This one was just more complex than I thought.

BTW - The fun part of getting this to work was having to upsample the phase ramp so that it doesn’t interpolate across the vertical jump from 1 to 0 and vice versa. I had to do kind of the opposite of a BLAMP. Sharpening the edge rather than smoothing it. Does anyone know of a reference for this in the literature? Anti-blamp Ramps to Smooth Out My Samps?

Sam

3 Likes

I made a major update to the Oversampling Oscillators. There are bug fixes and new UGens. The new UGens include:

OscOS3 - an Oversampled 3D Wavetable Oscillator
ShaperOS - an oversampled waveshaper
ShaperOS2 - a variable oversampled waveshaper

OscOS3 and ShaperOS2 add functionality and synthesis things that were not possible up until this point. That being said, there are a lot of moving parts and there may be some wrinkles, so let me know if anything seems messed up.

New help files for:

FM7aOS and FM7bOS - oversampled oscillator feedback-cluster UGens

Windows and Mac releases are here:

https://github.com/spluta/OversamplingOscillators/releases

Code is here:

https://github.com/spluta/OversamplingOscillators

BufFFT is needed to make the 3d wavetables.

Hopefully I didn’t break anything when I thought NRT wasn’t working on Windows.

Sam

7 Likes

These are amazing, thank you for your dedicated work @Sam_Pluta !

I would love to try the Oversampling Oscillators but I am having trouble building the plugins, and I should say that I never managed to build any SC plugins even though I tried a handful of times…there must be something fundamental I don’t get.

In this documentation it says
‘run the following from this directory to build from source using cmake’. What is ‘this directory’?. I tried entering this from the terminal after navigating to the extensions folder (I am on OSX).

mkdir build
cd build
cmake … -DCMAKE_BUILD_TYPE=Release -DSC_PATH=<“/Applications/SuperCollider/SuperCollider.app/”>
cmake --build . --config Release

and got this:

-bash: cmake: command not found

I have cmake 3.29.00 apparently.

Any help will be greatly appreciated. I really feel stupid for not managing something that should be pretty straight forward.