Code review of modulatable window functions

hey, i have made this implementation of a modulatable tukey window. If you change or modulate the alpha value you can smoothly transition from a rectangular shape to a hanning window, which could be cool for granular synthesis. i think the implementation is correct, but if someone has any ideas in terms of effiency and coding style then let me know:

(
var tukeyWindow = { |phase, alpha|

	var x = 0.5 * (1 - cos(phase * 2pi / alpha));
	var y = 0.5 * (1 - cos((phase - 1) * 2pi / alpha));

	var w1 = x * (phase < (alpha / 2));
	var w2 = (1 - w1) * (phase >= (alpha / 2)) * (phase <= (1 - (alpha / 2)));
	var w3 = y * (phase > (1 - (alpha / 2)));

	var result = w1 + w2 + w3;
	
	result * (phase < 1);
};

{
	var alpha = 0.5;
	var phase = Sweep.ar;
	tukeyWindow.(phase, alpha);
}.plot(1);
)

EDIT: modulating the window can sound like a low pass filter, or if you put it the other way around: sharper edges mean more spectral content:

3 Likes

i have also started to write this summed cosine window function which is maybe cool if you modulate the coeffiecients or throw in some random ones. to modulate the coefficients i think i have to get rid of the if statement for the sign.

(
//var coefs = [0.5, 0.5]; // hanning window
//var coefs = [0.54, 0.46]; // hamming window
var coefs = [0.21557895, 0.41663158, 0.277263158, 0.083578947, 0.006947368]; // flat top
//var coefs = [0.21557895, -0.41663158, 0.277263158, -0.083578947, 0.0069473684, -0.0001578947]; // random

var sumCosWindow = { |phase|
	var cosineTerms = coefs.drop(1).collect{ |coef, index|
        var sign = index.odd.if( { 1 }, { -1 });
		coef * sign * cos(phase * (index + 1) * 2pi);
    };
    coefs[0] + cosineTerms.sum;
};

{
    var phase = Sweep.ar;
    sumCosWindow.(phase);
}.plot(1);
)

i know that the purpose of these windows isnt necessarily granular synthesis but i thought i would try out some modulation ideas.

Hi, @dietcv. What would you recommend for freeing the single partial SynthDef after the window finishes (so it would act somehow like Osc1)?

i have been working on these window functions so you could use them for pulsar synthesis with audio rate. As far as I understand the question, it assumes that you would like to free the Synth after one grain for beeing used with Pbind for example. Ive been exploring the possibilties of audio and control rate for microsound in this thread Making transitions with language or server side sequencing

the most basic implementation with the tukey window for pulsar synthesis using audio rate triggers from Impulse and a triggered Sweep for the Phase, would be something like this:

(
var tukeyWindow = { |phase, alpha|

	var x = 0.5 * (1 - cos(phase * 2pi / alpha));
	var y = 0.5 * (1 - cos((phase - 1) * 2pi / alpha));

	var w1 = x * (phase < (alpha / 2));
	var w2 = (1 - w1) * (phase >= (alpha / 2)) * (phase <= (1 - (alpha / 2)));
	var w3 = y * (phase > (1 - (alpha / 2)));

	var result = w1 + w2 + w3;
	
	result * (phase < 1);
};

{
	var alpha = SinOsc.ar(0.1).linlin(-1, 1, 0.01, 1);
	
	var tFreq = \tFreq.kr(100);
	var trig = Impulse.ar(tFreq);
	var grainFreq = \grainFreq.kr(400);
	var overlap = \overlap.kr(4).clip(0, grainFreq / tFreq);
	
	var phase = Sweep.ar(trig, grainFreq);
	var windowPhase = phase / overlap;
	var grainWindow = tukeyWindow.(windowPhase, alpha);
	
	var sig = sin(phase * 2pi);
	sig = sig * grainWindow;
	
	sig!2 * 0.1;
}.play;
)

you could also swap sin(phase * 2pi) for BufRd to use other waveforms for the pulsaret:

(
var tukeyWindow = { |phase, alpha|

	var x = 0.5 * (1 - cos(phase * 2pi / alpha));
	var y = 0.5 * (1 - cos((phase - 1) * 2pi / alpha));

	var w1 = x * (phase < (alpha / 2));
	var w2 = (1 - w1) * (phase >= (alpha / 2)) * (phase <= (1 - (alpha / 2)));
	var w3 = y * (phase > (1 - (alpha / 2)));

	var result = w1 + w2 + w3;
	
	result * (phase < 1);
};

SynthDef(\basic_pulsar, { |sndBuf|
	
	var tFreq = \tFreq.kr(100);
	var trig = Impulse.ar(tFreq);
	var grainFreq = \freq.kr(400);
	var phase = Sweep.ar(trig, grainFreq);
	var overlap = \overlap.kr(3).clip(0, grainFreq / tFreq);
	
	var windowPhase = phase / overlap;
		
	var alpha = SinOsc.ar(0.1).linlin(-1, 1, 0.01, 1);
	var grainWindow = tukeyWindow.(windowPhase, alpha);
	
	var sig = BufRd.ar(
		numChannels: 1,
		bufnum: sndBuf,
		phase: phase * BufFrames.kr(sndBuf),
		loop: 1,
		interpolation: 4
	);
	
	sig = sig * grainWindow * \amp.kr(-15.dbamp);
	
	sig = Pan2.ar(sig, \pan.kr(0));
	OffsetOut.ar(\out.kr(0), sig);
}).add;
)

~sndBuf = Buffer.loadCollection(s, Signal.sineFill(4096, [1,0], 0!2));

Synth(\basic_pulsar, [\sndBuf, ~sndBuf]);

You could of course extend this basic setup using Pulsedivider for offsetting the triggers to overlap the windows inside the SynthDef, Demand Ugens to sequence different parameters like spatialization per grain or also fx processing per grain, or use heavy modulation of the basic parameters like trigger frequency, grain frequency and window multiplication (overlap)

EDIT: I think the example for pulsar synthesis from the helpfile of Osc1 is a bit misleading in terms of dealing with audio and control rate triggers. I think when wanting to build an granular instrument which is most versatile in terms of timbral transformation its better to use audio rate triggers from Impulse together with Sweep and Demand Ugens. Of course you could also use the ordinary GrainBuf, GrainSin etc. Ugens but these sample and hold each grain when triggered, so you could for example not modulate the window or do FM / PM per grain Frequency instead of phase - #31 by dietcv which is for example cool if you overlap several frequency sweeps for a quasi shepard-tone glissando.
This approach ist not binded to “pulsar synthesis” at all you could use it for all the other granular approaches as well, you just have to change the BufRd implementation:


(
var tukeyWindow = { |phase, alpha|

	var x = 0.5 * (1 - cos(phase * 2pi / alpha));
	var y = 0.5 * (1 - cos((phase - 1) * 2pi / alpha));

	var w1 = x * (phase < (alpha / 2));
	var w2 = (1 - w1) * (phase >= (alpha / 2)) * (phase <= (1 - (alpha / 2)));
	var w3 = y * (phase > (1 - (alpha / 2)));

	var result = w1 + w2 + w3;

	result * (phase < 1);
};

SynthDef(\basic_granular, { |sndBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainFreq = \grainFreq.kr(1);
	var phase = Sweep.ar(trig, grainFreq);
	var overlap = \overlap.kr(0.5);

	var windowPhase = phase / overlap;
	var grainWindow = tukeyWindow.(windowPhase, 0.5);

	var startPhase = TRand.ar(0, 80000, trig);
	var sig = BufRd.ar(
		numChannels: 1,
		bufnum: sndBuf,
		phase: phase * BufSampleRate.kr(sndBuf) + startPhase,
		loop: 0,
		interpolation: 4
	);

	sig = sig * grainWindow * \amp.kr(-10.dbamp);

	sig = Pan2.ar(sig, \pan.kr(0));
	OffsetOut.ar(\out.kr(0), sig);
}).add;
)

~sndBuf = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

Synth(\basic_granular, [\tFreq, 10, \overlap, 0.02, \sndBuf, ~sndBuf]);

so i think there is not any real benefit of using Osc1. Because Synthdefs are fixed with evaluation i think its nice to have an additional OffsetOut Ugen for the triggers, send them to a bus and have different modulator SynthDefs using Demand Ugens receiving these triggers to sequence different parameters via bus.asMap.

2 Likes