New quark: ddwWavetableSynth

I just put up some new code on github, to allow access to wavetable files such as those that ship with Serum, Vital and such.

Typically these synths crossfade between a number of “wavetable positions.” The wavetable source file usually concatenates one “wtpos” after another. In that case, it’s pretty straightforward to unpack them.

What we don’t have in SC, to my knowledge, is an oversampling BufRd to reduce aliasing at high frequencies. Should such a UGen emerge (please! that would be really nice!), then some of this quark would become unnecessary.

To reduce aliasing, this quark expands each single wavetable into several versions, removing successively more high-frequency content so that higher pitches can use more strongly-filtered versions. It’s a bit of a dodge – when sweeping the oscillator frequency, you can see the top frequencies fading in and out – but, if you lowpass-filter after that, it may be less noticeable (and it’s certainly less noticeable than heavy aliasing).

Two classes:

  • WavetablePrep, which reads a source multi-wavetable file and writes out an expanded wavetable file.

  • MultiWtOsc, a pseudo-UGen which plays back with frequency mapping, wtpos interpolation, phase distortion, and detuning (you decide how many oscillators). It’s a bit CPU-hungry actually; I’d consider it for nasty growly single notes but it may not be ideal for dense harmony.

Or

Quarks.install("https://github.com/jamshark70/ddwWavetableSynth");

hjh

14 Likes

A short demo – one wavetable instrument, with a bit of phase distortion being modulated (slowly) and filter stuff. “Unison” detuning is at just 3 oscillators.

hjh

7 Likes

Absolutely love it.

I realized this morning that it would be very easy to support oscillator hardsync, so I just added it.

// hacking a triangle --> sawtooth wavetable
(
f = SoundFile.openWrite("~/tri-saw.wav".standardizePath, "wav", "float", 1, 44100);

if(f.notNil) {
	var wt = Signal.newClear(2048);
	protect {
		50.do { |i|
			var frac = 0.5 - (i / 100);  // (0.5, 0.49 .. 0.01)
			var peakI = (2048 * frac).round.asInteger;
			var remain = 2047 - peakI;
			(0 .. peakI).do { |j|
				wt[j] = (j / peakI) * 2 - 1;
			};
			(peakI .. 2047).do { |j|
				wt[j] = ((j - peakI) / remain) * -2 + 1;
			};
			f.writeData(wt);
		};
	} { f.close };
};
)

// convert to MultiWtOsc format
w = WavetablePrep("~/tri-saw.wav".standardizePath, wtSize: 2048, numMaps: 8, ratio: 2, filter: \tenPctSlope).read;

w.write("~/tri-saw-wt.wav".standardizePath);

b = Buffer.read(s, "~/tri-saw-wt.wav".standardizePath);

// simple oscillator
(
a = {
	var sig = MultiWtOsc.ar(
		MouseX.kr(50, 800, 1),
		MouseY.kr(0, 48.999, 0),
		bufnum: b
	);
	(sig * 0.1).dup
}.play;
)

// hard sync
(
a = {
	// sync freq should be below oscillator freq
	var sync = LFTri.ar(50);
	var sig = MultiWtOsc.ar(
		MouseX.kr(50, 800, 1),
		MouseY.kr(0, 48.999, 0),
		bufnum: b,
		hardSync: sync
	);
	(sig * 0.1).dup
}.play;
)

// windowed sync
(
a = {
	var sync = LFTri.ar(50);
	var sig = MultiWtOsc.ar(
		MouseX.kr(50, 800, 1),
		MouseY.kr(0, 48.999, 0),
		bufnum: b,
		// try also with more oscillators
		// numOscs: 5, detune: 1.011,
		hardSync: sync
	);
	(sig * (sync * 5).clip(-1, 1) * 0.1).dup
}.play;
)

hjh

1 Like

That’s a great sound! And very useful to be able to load those wavetables.
Best,
Paul

Sounds great!
I just tried with a normal sound file like wavetable and it works very well :wink:
Thanks for sharing !!

Best

Oooooh :smiling_imp:

It’s with a bit of sadness that I note that I’ve lost touch with that experimental spirit of using devices incorrectly (so much so that I’m tempted, in my work in the near term at least, to embrace “correctness,” whatever that is). It didn’t even occur to me to try to use this with arbitrary audio.

FWIW last night I added a GUI, and the ability to read a wavetable matrix directly from an audio file – documentation isn’t there yet, but if you update the quark and install the ddwGUIEnhancements quark, then:

s.boot;

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav", 14327, 131072);

w = WavetablePrep.readFromProcessedFile(Platform.resourceDir +/+ "sounds/a11wlk01.wav", startFrame: 14327, numFrames: 131072);

(
a = { |freq = 440, wtPos = 0|
	(MultiWtOsc.ar(freq, wtPos, bufnum: b, numOscs: 3, detune: 1.008) * 0.1).dup
}.play;

g = w.gui(nil, Rect(800, 200, 500, 400)).front;

c = SimpleController(g)
.put(\freq, { |obj, what, freq| a.set(\freq, freq) })
.put(\wtPos, { |obj, what, wtPos| a.set(\wtPos, wtPos) })
.put(\didClose, { a.release });

a.onFree { c.remove };
)

(
p = Pbind(
	\type, \set,
	\id, [a.nodeID],
	\args, #[freq, wtPos],
	\freq, Pexprand(50, 500, inf),
	\wtPos, Pwhite(0, 6.999, inf),
	\dur, Pexprand(0.08, 0.4, inf),
	\finish, {
		defer(inEnvir {
			g.freq_(~freq).wtPos_(~wtPos)
		}, s.latency)
	}
).play;
)

p.stop;

g.close;  // also frees synth

This “incorrect” usage doesn’t avoid aliasing, of course, because there’s no longer any relationship between frequency mapping and frequency content – but it does sound nice and sci-fi glitchy.

I’m not sure how quickly I can get to it, but I also imagine that WavetablePrep could use Image in the same way that Serum can create wavetables by dragging an image file into an oscillator. Shouldn’t be hard – I’m just not sure if I will do it today or over the weekend.

hjh

4 Likes

OK! I think I’ve taken this quark about as far as I can for the time being.

Today I added image support (which was pretty easy, as I expected), and re-factored a little bit so that you can get wavetable data in and out of WavetablePrep purely in memory, without always having to use a temp disk file (similar to the way that CollStream lets you use an in-memory string as if it were a disk file).

I still need to think about normalizing the wavetables… maybe a minor update for this over the weekend.

But even without that, you can already do a lot of damage with this :grin:

hjh

1 Like

Beautiful, thanks for the new features, lot of fun !

Just added a phaseMod input, so now there is an opportunity to modulate phase both before phase distortion, and also after.

hjh

1 Like

I just pushed a small change, to map the wtPos fraction onto a sinusoidal curve for the crossfade instead of linear. This should make wtPos modulation a bit less zipper-y (though very rapid modulation still isn’t excellent).

hjh

1 Like

FWIW: I just fixed an extremely stupid mistake that prevented wtPos control-input changes from taking effect. (If anyone’s wondering: Duty.ar(SampleDur.ir, ...) is correct, but I had written Duty.ar(SampleRate.ir, ...) so the wtPos would be polled once per tens of thousands of seconds.)

hjh