Interpolating between Waveforms

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

mistertoast wrote: Mostly I agree. Interpolation does smooth out the transition, but if you look at the frequencies, you might be bringing up multiple partials, rather than the ideal one at a time. But practically, a lot of what happens at the top of an oscillator will be masked by a low pass filter in many cases. So it depends on the whole path. I rarely hear a naked osc in music.
Yes, but I am too lazy to write the program which analyzes these statistics and optimizes table size based on adaptation of patch and usage in realtime : ^)

Therefore, the right approach in my perspective is to write the solution which consistently sounds good.

There's a lot of content (good and bad) that will never be outright audible when placed in a realtime/performance/ensemble context, but I believe the quality is important and is perceived and sensed, even if it is not obvious to the listener. The same could be said of a string ensemble; the average listener can tell whether it sounds 'good' (though subjective), but she/he won't be able to actively hypothesize who built the instruments from listening to a recording alone.

The use of table sizes larger than 4096 for maps with low fundamentals is worth the memory consumption, IMO, and accuracy above 5k is important in this context, even if the amplitude difference for these partials is regularly 24 dB or greater when compared to the fundamental by the time it is mixed. Determining whether the patch uses specific harmonics is a special case that I would rather not implement, memory usage is not a problem for me right now, I enjoy the accuracy in the higher partials. Of course, sinusoid is another case altogether, as it has (ideally) no overtones.

The cool thing about better sounding instruments, is that you can often get away with 'less' in the sense that you don't need to add as much unisono or effects to get a instrument to sit in context. If the oscs sound like shite, then sound designers or mix engineers will (often unconsciously) mask what they find objectionable with effects, filtering or straight out amplitude reduction - a bit counteractive, IMO. If the synth does not reproduce the high end well, then it will get reduced while cymbals, guitars, claves and such will be pushed up to fill the void.

J

Post

>>cymbals, guitars, claves

Yeah, but that's not relevant in a bass synth. So I'm not confident that a one-size-fits-all oscillator solution makes sense.

I agree in the goal to shoot for quality, but what "quality" is can depend on the goals of the synth.

The Linn drum f**ked up because it didn't manage to kill aliasing. But the aliasing gave it a sizzle just right for its purpose.
Swing is the difference between a drum machine and a sex machine.

Post

Can you share that filter code?
Sure, it's in Java and a bit messy but should be clear enough. I'll post it when I get home from work.

Post

GameSmith wrote:
Can you share that filter code?
Sure, it's in Java and a bit messy but should be clear enough. I'll post it when I get home from work.
That would be awesome. Java and messy are both fine! :-)
Swing is the difference between a drum machine and a sex machine.

Post

exactly, for my synth, a 43 to 46Hz for the fundamental wavetable more than enough
sliding the oscillator is no problem (that's what it does _all_ the time)

i agree about different synths having different needs, what i explained works best for me ;]
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post

Here we go, I did a bit of a cleanup and haven't tested it but it should be working fine. You can generate the coefficients yourself for different factors using Matlab.

Code: Select all

public class DownSampler {

	private static final int FACTOR = 8;
	
	private static final int FIR_SIZE = 32;
	private static final int FIR_MASK = FIR_SIZE-1;
	
	// 32 long normalized Kaiser (param = 6.4) windowed sinc function
	private static final float COEFF[] = new float[]{-0.0f,1.06988606E-4f,
            4.9367646E-4f,0.0013906843f,0.0030779035f,0.0058494993f,
            0.009964011f,0.015586544f,0.022733344f,0.031230453f,0.040697157f,
            0.05056145f,0.060109124f,0.06856181f,0.07517301f,
            0.07932711f,0.08062468f,0.078939274f,0.07443558f,
            0.06754571f,0.058907907f,0.04927845f,0.039432116f,
            0.030067796f,0.021733874f,0.014783548f,0.009363905f,
            0.005436304f,0.00282024f,0.0012496994f,4.303706E-4f,8.779568E-5f,
	};
	
	
	private float[] input;
	private int index;
	
	public DownSampler(){
		reset();
	}
	
	//Down sample buffer to a single output sample. Assume buffer length = FACTOR.
	public float downSample(float[] buffer){
		float out;
		float s = buffer[0];
		input[index] = s;
		s *= COEFF[0];
		for (int Z = 1, z = index - 1; Z < FIR_SIZE; --z, ++Z){
			s += COEFF[Z] * input[z & FIR_MASK];
		}
		index = (index + 1) & FIR_MASK;
		out = s;
		
		for(int i = 1; i < FACTOR; i++){
			input[index] = buffer[i];
			index = (index + 1) & FIR_MASK;
		}
		return out;
	}

	public void reset() {
		Arrays.fill(input, 0);
		index = 0;
		
	}
	
	
}


Post

mistertoast wrote:>>cymbals, guitars, claves

Yeah, but that's not relevant in a bass synth. So I'm not confident that a one-size-fits-all oscillator solution makes sense.

I agree in the goal to shoot for quality, but what "quality" is can depend on the goals of the synth.

The Linn drum f**ked up because it didn't manage to kill aliasing. But the aliasing gave it a sizzle just right for its purpose.
They are relevant harmonics to my ears, including some (but not all) synth bass sounds. 5kHz *is* relevant in some bass sounds. The underlying point I was trying to make is that the signal to error ratio should be very low, and that the frequency response is important (especially since you do not always know what you are writing to the table ahead of time - it may be loaded with hf content). IMO, the wavetable bit of code should not introduce a readily identifiable sonic 'quality' - that work is for parts of the synth which have labels on the synth's block diagram... unless it is modeled after a PPG. It's also nice to have spectral wiggle room for a few reasons.

If I want a certain sound such as 'dull vintage saw' on osc 1 and '9 bit triangle' on osc 2, I would generate it that way and/or pre/post filter the generated sound or input sample appropriately. Furthermore, the harmonic strengths may vary greatly across the keyboard if/when the wavetable design is compromised in this manner - crossfading key-ranges becomes less practical when working with more complex waveforms and/or multiple oscs. The wavetable design I outlined is what worked best for my tastes/needs in wavetable implementation for general use. Sure, it and every aspect of a synth could be optimized for particular applications to no end, but this is what I consider the usable medium for a single wavetable write/playback object, and the good quality wavetable does work well (IMO). I like vintage digital more than the average musician, but I consider retro wavetable grunge and artifacts optional (but welcome) in a modern synth, especially considering the work I put into other areas, such as the generator. Of course, you may be on to a better design or have a good idea worth following through on and there's a lot of opinion/preference in each of our views/or ideas of a good synth bass. I would implement smaller tables to conserve memory when I knew the highest frequency that I needed to reproduce, but I don't like compromising the sound's integrity at this stage (the wavetable) because there are frequencies above the presence range that I like to hear in a synth bass (interpolation will have a lpf effect), and the windows and artifacts are pretty ugly in some cases. It's like additive; sometimes 4 partials is enough, sometimes it is 40, and sometimes it is more than 400 - at least with wavetables the CPU usage remains manageable (as opposed to synthesizing that many partials in an additive program).

J

Post

"The wavetable design I outlined is what worked best for my tastes/needs in wavetable implementation for general use."

Makes complete sense.
Swing is the difference between a drum machine and a sex machine.

Post

GameSmith wrote:Here we go...
Thanks!
Swing is the difference between a drum machine and a sex machine.

Post

My oscillators use fixed length wavetables, with decreasing number of harmonics (of course). Normally I'm creating one waveform per MIDI note (which may depend on the samplerate, e.g. if the last 20 waveforms would contain only 1 harmonic, there's only one waveform created).
During the creation I create a lookup table mapping pitch (in cents) to the correct wavetable index. So the pitch can easily be changed on a per sample basis and all I have to do is to calculate the new step. The step is a 16.16 fixed point number (which seemed to be sufficient).
This aproach sounds good on slides.

The major disadvantage of wavetables is: it kills the CPU cache. And I've tried the mip-mapped aproach once and noticed heavy performance drops compared to the fixed length stuff. (At least in my case).

For the sine wave: It's faster to do a simple taylor-approximation to calculate it in realtime. I've written a additive sine wave oscillator with 64 bands in assembler/SSE which is as fast as light :)
... when time becomes a loop ...
---
Intel i7 3770k @3.5GHz, 16GB RAM, Windows 7 / Ubuntu 16.04, Cubase Artist, Reaktor 6, Superior Drummer 3, M-Audio Audiophile 2496, Akai MPK-249, Roland TD-11KV+

Post

neotec wrote:For the sine wave: It's faster to do a simple taylor-approximation to calculate it in realtime. I've written a additive sine wave oscillator with 64 bands in assembler/SSE which is as fast as light.
Show the code! I'd like to see the results on an FFT. :-)

I like Moppel's phasor approach. http://www.tutututututu.de/synths/dsfsy ... hesis.html

Here's mine...
http://www.musicdsp.org/showArchiveComm ... hiveID=241
Swing is the difference between a drum machine and a sex machine.

Post

Ok, here it is:

Some explanations: This routine computes the sum of 64 sine waves, which are
an integer multiple of the base frequency, so 1x,2x,3x,...,64x. It expects a float[128] array, containing the band volumes and envelopes in following order: volume0-3, envelope0-3, volume4-7, envelope4-7, ...
The band volumes should sum up to 1, envelope between [0,1].

Phase is a UINT32, where 0 is 0*PI and 2^32 is 2*PI

It is also optimized to be cache friendly by branch-elimination.

... and it is fast (and accurate enough I think) :D

Code: Select all

; Concerning: ALIGN_BUG
;	ALIGN_BUG should be defined for 'buggy' linkers, e.g. g++ 4.3.2
;	gcc4.3.2 simply ignores (or miscalculates) the data segment alignment so
;	movaps can't be used, instead movups must be used (half as fast *duh*)
;	Note to gcc developers: F I X  T H I S, please

[bits 32]

[section .bss align = 16]
; nuthang
[section .data align = 16]
txmm1:		dd 	0, 0, 0, 0
txmm_null:	dd 	0, 0, 0, 0
fpi_xmm:	dd	0x3fc90fdb, 0x3fc90fdb, 0x3fc90fdb, 0x3fc90fdb
frf3_xmm:	dd	0xbe2aaaab, 0xbe2aaaab, 0xbe2aaaab, 0xbe2aaaab
frf5_xmm:	dd	0x3c088889, 0x3c088889, 0x3c088889, 0x3c088889
frf7_xmm:	dd	0xb9500d01, 0xb9500d01, 0xb9500d01, 0xb9500d01
frf9_xmm:	dd	0x3638ef1d, 0x3638ef1d, 0x3638ef1d, 0x3638ef1d
f1_xmm:		dd	0x3f800000, 0x3f800000, 0x3f800000, 0x3f800000
[section .text align = 16]

	global	_addOsc64_2_sse
	; float addOsc64_2_sse(int phase, float volumes[128]);
_addOsc64_2_sse:
	pushad
	mov 	ebp, esp
	mov		eax, [ebp + 0x24]		; phase
	mov		edi, eax
	mov		edx, [ebp + 0x28]
	mov		esi, 64 / 4
	
%ifdef ALIGN_BUG
	movups	xmm7, [txmm_null]
	movups	xmm6, [frf3_xmm]
	movups	xmm5, [frf5_xmm]
	movups	xmm4, [frf7_xmm]
	movups	xmm3, [f1_xmm]
%else
	movaps	xmm7, [txmm_null]
	movaps	xmm6, [frf3_xmm]
	movaps	xmm5, [frf5_xmm]
	movaps	xmm4, [frf7_xmm]
	movaps	xmm3, [f1_xmm]
%endif

	fld1
	fld		dword[fpi_xmm]

.loop:
	; Sine 0
	mov		ebx, 0x00800000
	bt		eax, 30
	sbb		ebx, 0
	mov		ecx, eax
	shr		ecx, 7
	xor		ebx, ecx
	or		ebx, 0x3f800000
	mov		[txmm1], ebx
	fld		dword[txmm1]
	fsub	st2
	fmul	st1
	fstp	dword[txmm1]
	mov		ecx, eax
	and		ecx, 0x80000000
	xor		[txmm1], ecx
	add		eax, edi
	adc		eax, 0
	; Sine 1
	mov		ebx, 0x00800000
	bt		eax, 30
	sbb		ebx, 0
	mov		ecx, eax
	shr		ecx, 7
	xor		ebx, ecx
	or		ebx, 0x3f800000
	mov		[txmm1 + 4], ebx
	fld		dword[txmm1 + 4]
	fsub	st2
	fmul	st1
	fstp	dword[txmm1 + 4]
	mov		ecx, eax
	and		ecx, 0x80000000
	xor		[txmm1 + 4], ecx
	add		eax, edi
	adc		eax, 0
	; Sine 2
	mov		ebx, 0x00800000
	bt		eax, 30
	sbb		ebx, 0
	mov		ecx, eax
	shr		ecx, 7
	xor		ebx, ecx
	or		ebx, 0x3f800000
	mov		[txmm1 + 8], ebx
	fld		dword[txmm1 + 8]
	fsub	st2
	fmul	st1
	fstp	dword[txmm1 + 8]
	mov		ecx, eax
	and		ecx, 0x80000000
	xor		[txmm1 + 8], ecx
	add		eax, edi
	adc		eax, 0
	; Sine 3
	mov		ebx, 0x00800000
	bt		eax, 30
	sbb		ebx, 0
	mov		ecx, eax
	shr		ecx, 7
	xor		ebx, ecx
	or		ebx, 0x3f800000
	mov		[txmm1 + 12], ebx
	fld		dword[txmm1 + 12]
	fsub	st2
	fmul	st1
	fstp	dword[txmm1 + 12]
	mov		ecx, eax
	and		ecx, 0x80000000
	xor		[txmm1 + 12], ecx
	add		eax, edi
	adc		eax, 0
	; 4x SIN calc
%ifdef ALIGN_BUG
	movups	xmm1, [txmm1]		; x
%else
	movaps	xmm1, [txmm1]		; x
%endif
	movaps	xmm2, xmm1
	mulps	xmm2, xmm1			; x * x
	movaps	xmm0, xmm4			; frf7
	mulps	xmm0, xmm2			; frf7 * x2
	addps	xmm0, xmm5			; frf7 * x2 + frf5
	mulps	xmm0, xmm2			; (frf7 * x2 + frf5) * x2
	addps	xmm0, xmm6			; (frf7 * x2 + frf5) * x2 + frf3
	mulps	xmm0, xmm2			; ((frf7 * x2 + frf5) * x2 + frf3) * x2
	addps	xmm0, xmm3			; ((frf7 * x2 + frf5) * x2 + frf3) * x2 + 1.0f
	mulps	xmm0, xmm1			; (((frf7 * x2 + frf5) * x2 + frf3) * x2 + 1.0f) * x
	mulps	xmm0, [edx]			; (((frf7 * x2 + frf5) * x2 + frf3) * x2 + 1.0f) * x * vol
	mulps	xmm0, [edx + 16]
	addps	xmm7, xmm0
	; loop
	add		edx, 32
	dec		esi
	jnz		.loop
	; final summation
%ifdef ALIGN_BUG
	movups	[txmm1], xmm7
%else
	movaps	[txmm1], xmm7
%endif
	fstp	st0
	fstp	st0
	fld		dword[txmm1]
	fadd	dword[txmm1 + 4]
	fadd	dword[txmm1 + 8]
	fadd	dword[txmm1 + 12]
	; done
	popad
	ret
... when time becomes a loop ...
---
Intel i7 3770k @3.5GHz, 16GB RAM, Windows 7 / Ubuntu 16.04, Cubase Artist, Reaktor 6, Superior Drummer 3, M-Audio Audiophile 2496, Akai MPK-249, Roland TD-11KV+

Post

Very cool. I'll see if I can get around to trying it this weekend.
Swing is the difference between a drum machine and a sex machine.

Post

Here's a test using the approach described in my original post. I use the pitch wheel to bend down/up two octaves:

Wavetable Test 01

It's a wav file, so it while take a bit longer to download.

CPU usage seems pretty good, about 1% per voice on my 6+ year old P4. That's of course without any filters and other components.

P.S. If you hear any aliasing, distortion, or other weirdness, let me know. I'm still polishing up my approach.

Post

Leslie Sanford wrote:Here's a test using the approach described in my original post. I use the pitch wheel to bend down/up two octaves:

Wavetable Test 01

It's a wav file, so it while take a bit longer to download.

CPU usage seems pretty good, about 1% per voice on my 6+ year old P4. That's of course without any filters and other components.

P.S. If you hear any aliasing, distortion, or other weirdness, let me know. I'm still polishing up my approach.
Hey Leslie,

It is easiest to evaluate this critically using a single osc, playing a single note, with a sine demo and a saw demo recorded at 44.1kHz, while spending a good amount of time in animation, and hitting many tones. The current demo spends a lot of time at the same tone (which may or may not demonstrate the interpolation well).

J

Post Reply

Return to “DSP and Plugin Development”