Demand partial mask

hey, im trying to come up with a trigger based “partial mask” for additive synthesis and granulation.
The idea is to select a random partial or a cluster from ratios per trigger and then set all the amplitudes in amps to zero, beside the one amplitude from the partial or the array of amplitudes from the cluster beeing selected, which should keep its or their current value. I know how i can select a random partial from ratios to index into the amps array, but i dont know how i can set the remaining amplitudes to zero. does somebody have an idea? thanks

EDIT: i think cluster would mean neighbour left and neighbour right. which is even more tricky for the first or the last index. maybe lets stay with the single partial for now.

(
var partialMask = { |amps, ratios, trig|
	var ampIndex = Demand.ar(trig, 0, Dxrand(ratios - 1, inf));

	ampIndex.poll(trig, \ampIndex);

	amps;
};

var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	var numPartials = 16;
	var ratios = (1..numPartials);
	var amps = 1 ! numPartials;

	var tFreq = 1;
	var trig = Impulse.ar(tFreq);
	var phase = Sweep.ar(trig, tFreq);

	var grainWindow = hanningWindow.(phase);

	var freqs = \freq.kr(220) * ratios;

	var sig = SinOsc.ar(freqs);

	amps = partialMask.(amps, ratios, trig);

	sig = (sig * amps).sum;

	sig = sig * grainWindow;

	sig !2 * 0.01;

}.play;
)

Could you use Select to do this? Indexing into the sig array?

You could try a bit mask. If there is an array of size 10, then the corresponding mask is of length 10, which gives [ 0!10 ]. You probably can use an array as a bit mask and see if there’s an elementwise multiplilcation, but the traditional way is to have an integer, whose binary represents the mask, i.e. 0000000001 is decimal 1, 0000100000 is decimal 32, etc, and bitshift that to get either [1,0], 32>>5 and use that to zero out the amplitudes of the bins.

hey, i havent thought about that before. thanks alot :slight_smile: Im actually not sure, but think i would like to keep my initial idea when possible. The “partial mask” would be another building block in a cascade of functions which all manipulate an “amplitude event”. i would like to stay within that logic if possible.
I have already a bunch of additive filters and multiplying their magnitude responses with the chained amplitude event and finally pass chain[\freqs] and chain[\amps] to SinOsc before i mix them down.

like this:

(
var makeStretchedHarmonicSeries = { |numPartials, freq, inharmonicity|
	var ratios = (1..numPartials);
	var chain = (
		numPartials: numPartials,
		ratios: ratios,
		amps: 1 ! numPartials,
	);
	var stretchedRatios = ratios * (1 + (inharmonicity * ratios * ratios)).sqrt;
	chain[\freqs] = stretchedRatios *.t freq;
	chain;
};

var addSpectralTilt = { |chain, tiltPerOctave|
	chain[\amps] = chain[\amps] * (chain[\ratios].log2 * tiltPerOctave).dbamp;
	chain;
};

[...]

{

	[...]
	
	chain = makeStretchedHarmonicSeries.(numPartials, freq, \inharmonicity.kr(0.01));
	chain = addSpectralTilt.(chain, \tiltPerOctaveDb.kr(-3));
	chain = addBandPassFilter.(chain, ....);
	chain = addPartialMask.(chain, ....);

	sigs = SinOsc.ar(
		freq: chain[\freqs],
		phase: { Rand(0, 2pi) } ! chain[\numPartials],
		mul: chain[\amps]
	).sum;
	
	[...]

};
)

hey thanks for your reply, this looks quite tricky. i will have a deeper look.

Here is a funny solution:

(

var partialMask = { |amps, ratios, trig|

	var selectedPartial = Demand.ar(trig, 0, Dxrand(ratios, inf));
	selectedPartial.poll(trig, \selectedPartial);
	PanAz.ar(16, DC.ar(1), (selectedPartial/8).poll, 1, 2, -1);
};

var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	var numPartials = 16;
	var ratios = (1..numPartials);
	var amps = K2A.ar([1]++(0!(numPartials-1)));
	var tFreq = 1;
	var trig = Impulse.ar(tFreq);
	var phase = Sweep.ar(trig);
	var grainWindow = hanningWindow.(phase);
	var freqs = \freq.kr(220) * ratios;
	var sig = SinOsc.ar(freqs);
	amps = partialMask.(amps, ratios, trig);
	sig = (sig * amps).sum;
	sig = sig * grainWindow;
	sig !2 * 0.01;
}.play;

)

Sam

edit: fixed a lil bug

thanks for all your suggestions :slight_smile: I thought it would be possible to transfer the following sketch i made which is using numPartials Dust triggers to an implementation which is just using a single Impulse trigger with an additional “partial mask”, which would enable me to implement it in my current additive synthesis granulation approach. I thought masking the spectrum to a single partial would do it, but didnt know that it would be that tricky…

(
var makeStretchedHarmonicSeries = { |numPartials, freq, inharmonicity|
	var chain = (
		numPartials: numPartials,
		ratios: (1..numPartials),
		amps: 1 ! numPartials,
	);
	var ratios = (1..numPartials);
	var stretchedRatios = ratios * (1 + (inharmonicity * ratios * ratios)).sqrt;
	chain[\freqs] = stretchedRatios * freq;
	chain;
};

var addSpectralTilt = { |chain, tiltPerOctave|
	chain[\amps] = chain[\amps] * (chain[\ratios].log2 * tiltPerOctave).dbamp;
	chain;
};

var granulate = { |chain, blend, rate|
	var trig = Dust.kr(rate ! chain[\numPartials]);
	var grainWindow = blend(
		DC.kr(1),
		EnvGen.kr(Env([0, 1, 0], [0.001, 0.999], [4.0, -4.0]), trig, doneAction: Done.none),
		blend
	);
	chain[\amps] = chain[\amps] * grainWindow;
};

SynthDef(\additive_granulation, {

	var numPartials = 50;
	var chain, sig;

	chain = makeStretchedHarmonicSeries.(numPartials, \freq.kr(440), \inharmonicity.kr(0.005));
	chain = addSpectralTilt.(chain, \tiltPerOctaveDb.kr(-3));
	chain = granulate.(chain, \grainOn.kr(1), \grainRate.kr(1));

	sig = SinOsc.ar(
		freq: chain[\freqs],
		phase: { Rand(0, 2pi) } ! chain[\numPartials],
		mul: chain[\amps]
	).sum;
	
	sig = Pan2.ar(sig, \pan.kr(0));
	
	sig = sig * \amp.kr(-15.dbamp);
	
	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

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

Synth(\additive_granulation, [\freq, 44.midicps]);

This is my additive granulation setup where i cant easily implement the granulate function, because you have already granulation in place with timingInformation + IEnvGen here. I think it makes no sense to also add the granulate function to the chain, then the IEnvGen grain window and the granulate grain window are not related to each other.
I would like to have the option to crossfade between granulation of the summed spectrum and granulation of the spectrum itself whithout adding another unrelated envelope / window with EnvGen and using additional unrelated triggers with Dust. It should all work with Impulse + timingInformation + IEnvGen.

(
var makeStretchedHarmonicSeries = { |numPartials, freq, inharmonicity|
	var chain = (
		numPartials: numPartials,
		ratios: (1..numPartials),
		amps: 1 ! numPartials,
	);
	var ratios = (1..numPartials);
	var stretchedRatios = ratios * (1 + (inharmonicity * ratios * ratios)).sqrt;
	chain[\freqs] = stretchedRatios * freq;
	chain;
};

var addSpectralTilt = { |chain, tiltPerOctave|
	chain[\amps] = chain[\amps] * (chain[\ratios].log2 * tiltPerOctave).dbamp;
	chain;
};

var timingInformation = { |numChannels, trig, grainRate|
	var rate = if(trig.rate == \audio, \ar, \kr);
	var arrayOfTrigsAndPhases = numChannels.collect{ |i|
		var localTrig = PulseDivider.perform(rate, trig, numChannels, i);
		var hasTriggered = PulseCount.perform(rate, localTrig) > 0;
		var localPhase = Sweep.perform(rate, localTrig, grainRate * hasTriggered);
		[localTrig, localPhase];
	};
	var trigsAndPhasesArray = arrayOfTrigsAndPhases.flop;
	(\trigger : trigsAndPhasesArray[0], \phase: trigsAndPhasesArray[1]);
};

var addPartialMask = { |chain, trig, maskOn|
	var selectedPartial = Demand.ar(trig, 0, Dxrand(chain[\ratios] - 1, inf));
	var mask = chain[\numPartials].collect{ |partial|
		InRange.ar(selectedPartial, partial, partial);
	};
	selectedPartial.poll(trig, \selectedPartial);
	chain[\amps] = chain[\amps] * blend(DC.ar(1), mask, maskOn);
	chain;
};

SynthDef(\additive_granulation, {

	var	numChannels = 5;
	var numPartials = 50;

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainRate = tFreq / \overlap.kr(1);

	var timings = timingInformation.(numChannels, trig, grainRate);

	var grainWindows = IEnvGen.ar(Env([0, 1, 0], [0.001, 0.999], [4.0, -4.0]), timings.phase);

	var chain, sigs, sig;

	chain = makeStretchedHarmonicSeries.(numPartials, \freq.kr(440), \inharmonicity.kr(0.005));
	chain = addSpectralTilt.(chain, \tiltPerOctaveDb.kr(-3));
	chain = addPartialMask.(chain, trig, \maskOn.kr(1));

	sigs = SinOsc.ar(
		freq: chain[\freqs],
		phase: { Rand(0, 2pi) } ! chain[\numPartials],
		mul: chain[\amps]
	).sum;

	sigs = sigs * grainWindows;

	sigs = PanAz.ar(2, sigs, \pan.kr(0));
	sig = sigs.sum;

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

	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

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

(
Synth(\additive_granulation, [

	\tFreq, 10,
	\overlap, 5,
	\maskOn, 1,

	\freq, 44.midicps,

]);
)

I have used Demand and InRange to select a single Partial and set all the other amplitudes to zero. But this sounds really different compared to the numPartial times Dust triggers because you dont get the overlapping windows.

Do you have any suggestions?