Wavetable multicycle

Sorry if this is a newbie question.

I’m a bit confused about wavetables. I’ve found quite a few tutorials about loading wavetables and morphing between two of them, for example.
But I think those tutorials mostly deal with single-cycle wavetables. Aren’t there also multi-cycle ones?
Do they have a specific name? I mean the kind where you can smoothly morph within a single wavetable file through many waveforms.

I’m not 100% sure how that works, but I was wondering whether this is possible in SuperCollider.

“Is it possible”… sure, why wouldn’t it be? The question is really, who has done it and made it easier?

Sam Pluta’s Oversampling Oscillators has a couple of 2D wavetable oscillators. GitHub - spluta/OversamplingOscillators

I have a quark (“ddwWavetableSynth”) that lets you put multiple wavetables into one buffer, and mipmap them for reduced aliasing (but without oversampling). Effectively this is 2D but one of those dimensions is reserved for reducing aliasing, so it isn’t directly modulatable. You can morph freely across one dimension.

There may be other solutions that I’m overlooking at the moment.

hjh

1 Like

Hey jamshark70.

thank you for your help. I think I got a bit closer. I am able to play the wavtable, but my issue is the loading time. it takes quite a while, like 10-15 seconds just to load one of those wavetable files. I am sure I am doing something wrong.

Basically, I am trying to load a standard 256-frame wavetable file (Mono, 2048 samples per frame) into SuperCollider to use with VOsc

Loading that into an array takes forever to process, I would basically load
over 500.000 samples into an array.

If I load it directly to the Server using Buffer.read, the quality is poor because the buffers aren’t in the correct Wavetable format that VOsc requires.

Is there a solution for that?

Either my quark or the oversampling oscillators package.

In my quark, there’s a helper class that will process the wavetables once (for mipmapping) and write it back to disk (not using wavetable format). Then you just load the one file into one buffer, done, no overhead for repeated use.

The OscOS family also works with a series of wavetables in one buffer, also not using wavetable format.

Btw my quark doesn’t install any new server plugins, but it uses a chunk of UGens to get the job done. OscOS is all about server plugins – smaller SynthDefs (but in my testing, not necessarily lower CPU use).

In any case, I think you’d probably save some time by checking out one or the other of these approaches. I wouldn’t recommend trying to manage 256 wavetables in 256 buffers.

hjh

1 Like

That sounds great. Thanks a lot. Will give that a try :slight_smile:

Thats comparing apples with oranges imo. OscOS has sinc interpolation and dynamic mipmapping, which is excellent for anti-aliasing without even using oversampling.

I have made some further improvements to SingleOscOs with bit masking for the buffer lookup in the hot for loop for sinc interpolation for better performance. The following example gives me about 2.5 % for no oversampling and about 8% for 4x oversampling. The anti-aliasing should be way better compared to MultiWtOsc without even using oversampling. If you then want to use FM/PM oversampling can additionally improve the overall sound.

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

(
a = {
	var freq = MouseX.kr(50, 6000, 1);
	var bufloc = LFTri.kr(0.05).range(0.0, 1.0);
	var numOscs = 5, detun = 1.008;
	var detunes = Array.fill(numOscs, { detun ** Rand(-1.0, 1.0) });
	var phase = Phasor.ar(0, (freq * SampleDur.ir) * detunes, 0, 1);

	var sig = SingleOscOS.ar(b, phase, BufFrames.kr(b) / 2048, bufloc, 0);

	(sig.asArray.sum * 0.1).dup
}.play;
)

(
a = {
	var freq = MouseX.kr(50, 6000, 1);
	var bufloc = LFTri.kr(0.05).range(0.0, 1.0);
	var numOscs = 5, detun = 1.008;
	var detunes = Array.fill(numOscs, { detun ** Rand(-1.0, 1.0) });
	var phase = Phasor.ar(0, (freq * SampleDur.ir) * detunes, 0, 1);

	var sig = SingleOscOS.ar(b, phase, BufFrames.kr(b) / 2048, bufloc, 2);

	(sig.asArray.sum * 0.1).dup
}.play;
)
1 Like

You’re correct that the two approaches aren’t sonically equivalent – if I gave that impression, I’m happy to clarify that it wasn’t intentional.

MultiWtOsc sacrifices energy in the top octave below Nyquist in exchange for reduced aliasing – I believe what you’ll get with MultiWtOsc is some reduced “air” above 11 kHz, and limited aliasing at some fundamental frequencies, with aliasing mostly limited to that top octave. It is “worse” than sinc interpolation but saying that it’s “way” worse I think is a bit vague (what is “way worse”?), and might not be substantiated by concrete measurements. (I downloaded a nice VST frequency analyzer just last week; maybe I’ll try it with these UGens later today.)

It’s up to each user to decide what matters to them more. I tend to run a lot of polyphony (slow-envelope pads with a pretty high degree of overlap), and the mipmapping has been clean enough for my purposes, so I’ve stayed with MultiWtOsc to save CPU cycles. If your use case is, say, a wobble bass, then it’s monophonic so the CPU cost would be less severe, and that probably involves phase modulation or phase distortion, where the oversampling would be more important.

hjh

1 Like

OK, since you got my curiosity up, I concocted a little test.

I started with a gapped sawtooth spectrum, every third harmonic. I figured that would have enough empty space in the spectrum to see aliasing in the upper extreme range.

I played around with MouseX for a while, and found a frequency that had a good spectrum with MultiWtOsc:

Then tried the same frequency with OscOS (without oversampling, only its on-the-fly mipmapping):

And with 4x oversampling:

Now, this frequency was empirically found to show ideal behavior in MultiWtOsc. So I looked for another frequency where some aliasing is visible = 1305.52 Hz.

So MultiWtOsc doesn’t block all aliasing… but neither does OscOS. (I won’t post graphs for OscOS, but they’re very similar to the others.)

MultiWtOsc mipmaps more aggressively – it cuts off more high frequencies than it needs to. This has the disadvantage of losing a bit of brilliance in the top end (probably – my hearing is pretty well gone above, oh, 12 kHz by now, not bad for my age but still), but the advantage of completely removing aliasing in some frequency ranges and mostly removing it in others. OscOS’s sound might be more consistent, but that consistency seems to include, ironically, more aliasing. (However OscOS should sound better at very, very high frequencies – I limited my tests to <= 6000 Hz; above that, I saw some weird behavior with my mipmapping alone.)

CPU:

  • Baseline (only freq analyzer): 5-6%
  • MultiWtOsc: 7-8%
  • OscOS (no OS): 14-16%
  • OscOS (4x OS): 37-40%

This doesn’t mean that two OscOS’s would hit 80% – that would probably cause the machine to raise the CPU clock speed – but it does show the magnitude of the CPU hit, and why I decided to stick with MultiWtOsc for my purposes.

Also curiosity up :wink: and I’d have to agree that, of these three UGens, SingleOscOS looks a lot better than OscOS, and CPU use isn’t heavy. But I think the two extra harmonics in this graph are harmonic distortion, not part of the original spectrum.

2876 Hz:

And 1305.52 Hz is icky for all 3 UGens:

So as for the statement that SingleOscOS would be “way” better – I just don’t see that in the graphs. When sweeping the frequency, SingleOscOS looks more consistent and smoother than MultiWtOsc, but I don’t see a stark contrast between “one looks great” and “the other looks awful.” I’d say “one looks good” and “mine looks slightly less good.” So they are different beasts and users should choose according to their needs.

hjh

// make a wavetable with every third harmonic
// a 2048-sample cycle can go up to 1024 cycles/window; stop a bit short of that
s.boot;

(
w = Signal.sineFill(2048, Array.fill(1020, { |i|
	if(i % 3 == 0) { (i+1).reciprocal } { 0 }
}));
)

// optional, see freq analysis
// w.fft(Signal.newClear(w.size), Signal.fftCosTable(w.size)).rho[0 .. (w.size div: 2)].plot

p = WavetablePrep(wtSize: w.size, numMaps: 8, ratio: 2, filter: \brickwall);
p.readStream(SoundFileStream(w));

m = SoundFileStream.new;
p.writeStream(m);

b = Buffer.sendCollection(s, m.collection, 1);

// 6000 Hz = midinote 114.232644 or F#, 4 octaves above mid C
// also try 1305.52 = dodgy; 2876 = nice
(
a = {
	var freq = 1305.52; // MouseX.kr(50, 6000, 1);
	var sig = MultiWtOsc.ar(freq, wtPos: 0,
		bufnum: b,
		wtSize: 2048,
		numTables: 8,
		ratio: 2,
		interpolation: 4
	);
	(sig * 0.04).dup
}.play;
)

a.free;

// OscOS dynamic mipmapping only
(
a = {
	var freq = 1305.52; // MouseX.kr(50, 6000, 1);
	var phase = Phasor.ar(0, freq * SampleDur.ir, 0, 1);
	var sig = OscOS.ar(b, phase, buf_divs: 8, buf_loc: 0);
	(sig * 0.08).dup
}.play;
)

a.free;

// OscOS mipmap + 4x oversampling
(
a = {
	var freq = 2876; // MouseX.kr(50, 6000, 1);
	var phase = Phasor.ar(0, freq * SampleDur.ir, 0, 1);
	var sig = OscOS.ar(b, phase, buf_divs: 8, buf_loc: 0, oversample: 4);
	(sig * 0.08).dup
}.play;
)

a.free;

(
a = {
	var freq = MouseX.kr(50, 6000, 1);
	var phase = Phasor.ar(0, freq * SampleDur.ir, 0, 1);
	var sig = SingleOscOS.ar(b, phase, 8, 0, oversample: 0);
	(sig * 0.08).dup
}.play;
)

a.free;

Hey, i really appreciate your effort :slight_smile: I didnt want to critize MultiWtOsc by any means and was maybe a bit sloppy with my words, sorry for that. My intention was to bring up the conversation once again which started in the oversampling oscillator repo OscOS and OscOS3 performance · Issue #5 · spluta/OversamplingOscillators · GitHub and to point out that i figured out at least one bottleneck which improves the performance significantly. The control rate parameters in SingleOscOS are internally interpolated this should give additional smoothness when changing the values at control rate.
I will have a further look at your test examples :slight_smile:

Thanks – I’ll admit that part of the motivation for my testing was based on that. But more important than that is to look at the concrete behavior – I, at least, have a better understanding of the capabilities and limitations of these three options. It’s good to have it out there for posterity. (Plus that Blue Cat analyzer is pretty nice, much more detailed than our FreqScope. Mac/Win only but it runs well on Linux with wine + yabridge.)

Also on my list to look into over the holiday: what’s the fastest safe modulation slope for the wavetable-position parameter? In MultiWtOsc, it’ll click if you cross more than 1 wtPos per audio sample. I’ve avoided wtPos envelopes because of that, but that’s half the fun. Gonna play with that soon, and probably add a slew limiter into the class.

Also, for that matter, this morning I found a bug in the default filter option for my WavetablePrep class. I was trying to extend the mipmapping up higher than 8 steps, and found that the tenPctSlope filter made an absolute hash of it in one (only one) position. So now I have a chance to fix that.

I agree – OscOS2 and OscOS3 have some unique features, which are currently “locked up” in a way behind the performance problems (in the sense that many users may simply just not use them outside of monophonic contexts).

hjh

Will commit a fix for this today. It’s kind of embarrassing actually – the idea was to ramp down over a number of bins rather than brick-walling it, but I had the ramp in the wrong place, and forgot to ramp down the upper half of the FFT.

I’d been dissatisfied with the sound of my wavetable synths previously. Now I guess it’s because they were filtered improperly.

hjh

hey, isnt there a way to move forward together instead of everybody on their own?
You have figured out some valuable things to create wavetables and are using mipmapping with SC classes. @Sam_Pluta implemented the oversampling oscillators, then i presented the sinc interpolation with dynanamic mipmapping from the go book in SC, this then found its way into the oversampling oscillators, then i have figured out a way for better performance using bitwise wrapping for power of 2 tables and now we are stuck with different attempts from different people.

I guess I see my quark as being a bit orthogonal to the oversampling approaches. In mine, the main feature is the mipmapping (which I’d been doing for a long time because the first computer on which I used SC could run only, oh, 6(?) Saw oscillators at once before getting dropouts – remember the G4 chip? :laughing: – so I used wavetables to get more saws). Since OscOS and SingleOscOS do the mipmapping internally, there’s no need for the WavetablePrep step in my quark. (But mine may still be useful when minimizing CPU use is a priority.)

What if you prepared a PR to update the oversampling oscillators package with your performance changes? It looks like Sam isn’t actively writing new code for that project at the moment – that is, one possible conversation is “hey @Sam_Pluta please fix this” (where a likely reply is “no time, sorry”), but another possible conversation is “here’s a working fix, what do you think?” which strikes me as more collaborative and friendlier.

It would be very nice to have an order of magnitude performance improvement in the 2D wavetable oscillator from the oversampling pack.

hjh

Okay, this makes sense :slight_smile:

The architecture of my 1D implementation is quite different from OscOS, im not sure if i can figure out where what has to be done without actually just rewriting what i already have. I could create a 2D or 3D version though, based on what i already got.