NOTAM Meetups: Spring 2025

hey, thanks everyone for attending :slight_smile: Unfortunately we have been running out of time, so we couldnt have a look at all the possibilities for extended modulation and listen to some of its crazy sounds.
Here are just a few of them, make sure to have the latest version of the two libraries. Ive just updated them.

These techniques unlock further possibilities for timbral transformation of pulsar synthesis and arent available in any of the exisiting commercial or non-commercial devices. I would encourage you to experiment with those :).

I will write an additional thread with my current pulsar SynthDef, which uses some of these things. Still work in progress :slight_smile:



// 1.5.) FM vs. PM

// 1.5.1.) phase shaping vs. FM with frequency window

(
var eventData = { |rate|

	var eventPhase, eventSlope, eventTrigger;

	eventPhase = VariableRamp.ar(rate);
	eventSlope = ~grainFunctions.helperFunctions[\rampToSlope].(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = ~grainFunctions.helperFunctions[\rampToTrig].(eventPhase);

	(
		phase: eventPhase,
		slope: eventSlope,
		trigger: eventTrigger
	);

};

{
	var tFreq, events;
	var subSampleOffset, accumulator;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainFreqMD, grainSlope, grainPhaseA, grainPhaseB;
	var freqWindow, grainFreqA, grainFreqB;
	var sigA, sigB;

	tFreq = 100;
	events = eventData.(tFreq);

	subSampleOffset = ~grainFunctions.helperFunctions[\subSampleOffset].(
		events[\phase],
		events[\slope],
		events[\trigger]
	);

	accumulator = ~grainFunctions.helperFunctions[\accumSubSample].(
		trig: events[\trigger],
		subSampleOffset: subSampleOffset
	);

	windowSlope = events[\slope] / max(0.001, \overlap.kr(1));
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = windowPhase < 1;

	grainFreq = \freq.kr(400);

	/////////////////////////////////////////////////////////////////////////

	// FM with "frequency window"

	freqWindow = ~unitShapers.windowFunctions[\exponential].(
		windowPhase,
		\pitchSkew.kr(0),
		\pitchShape.kr(1)
	);

	grainFreqA = grainFreq + (grainFreq * freqWindow * \grainFreqMD.kr(14));
	grainSlope = grainFreqA * SampleDur.ir;

	grainPhaseA = ~grainFunctions.helperFunctions[\rampSubSample].(
		trig: events[\trigger],
		slope: grainSlope,
		subSampleOffset: subSampleOffset
	);
	grainPhaseA = grainPhaseA.wrap(0, 1);

	/////////////////////////////////////////////////////////////////////////

	// Phase shaping

	grainPhaseB = ~unitShapers.lerpingFunctions[\exponential].(
		windowPhase,
		\phaseShape.kr(0)
	);
	grainPhaseB = grainPhaseB * (grainFreq / max(0.001, windowSlope * SampleRate.ir));
	grainPhaseB = grainPhaseB.wrap(0, 1);

	/////////////////////////////////////////////////////////////////////////

	sigA = sin(grainPhaseA * 2pi) * grainWindow;
	sigB = sin(grainPhaseB * 2pi) * grainWindow;

	//[grainPhaseA, grainPhaseB];
	[sigA, sigB];

}.plot(0.021);
)

// 1.5.2.) PM vs. FM with a tracking OnePole Filter

(
var eventData = { |rate|

	var eventPhase, eventSlope, eventTrigger;

	eventPhase = VariableRamp.ar(rate);
	eventSlope = ~grainFunctions.helperFunctions[\rampToSlope].(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = ~grainFunctions.helperFunctions[\rampToTrig].(eventPhase);

	(
		phase: eventPhase,
		slope: eventSlope,
		trigger: eventTrigger
	);

};

{
	var tFreq, events;
	var subSampleOffset, accumulator;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainSlope, pmod, fmod;
	var grainPhasePM_A, grainPhasePM_B, grainPhaseFM;

	tFreq = \tFreq.kr(100);
	events = eventData.(tFreq);

	subSampleOffset = ~grainFunctions.helperFunctions[\subSampleOffset].(
		events[\phase],
		events[\slope],
		events[\trigger]
	);

	accumulator = ~grainFunctions.helperFunctions[\accumSubSample].(
		trig: events[\trigger],
		subSampleOffset: subSampleOffset
	);

	/////////////////////////////////////////////////////////////////////////

	windowSlope = Latch.ar(events[\slope] / max(0.001, \overlap.kr(1)), events[\trigger]);
	windowPhase = (windowSlope * accumulator).clip(0, 1);

	/////////////////////////////////////////////////////////////////////////

	grainFreq = \freq.ar(400);
	grainSlope = grainFreq * SampleDur.ir;

	// PM & FM with tracking OnePole filter
	pmod = sin(windowPhase * 2pi) * \pmIndex.kr(2);
	pmod = pmod / 2pi * Latch.ar(grainSlope / windowSlope, events[\trigger]);
	pmod = OnePole.ar(pmod, exp(-2pi * windowSlope));

	fmod = sin(windowPhase * 2pi) * \fmIndex.kr(2);
	fmod = fmod - OnePole.ar(fmod, exp(-2pi * windowSlope));

	/////////////////////////////////////////////////////////////////////////

	// PM_A
	grainPhasePM_A = (accumulator * Latch.ar(grainSlope, events[\trigger])).wrap(0, 1);
	grainPhasePM_A = (grainPhasePM_A + pmod).wrap(0, 1);

	// PM_B
	grainPhasePM_B = (windowPhase * Latch.ar(grainSlope / windowSlope, events[\trigger])).wrap(0, 1);
	grainPhasePM_B = (grainPhasePM_B + pmod).wrap(0, 1);

	// FM
	grainPhaseFM = ~grainFunctions.helperFunctions[\rampSubSample].(
		trig: events[\trigger],
		slope: grainFreq + (grainFreq * fmod) * SampleDur.ir,
		subSampleOffset: subSampleOffset
	);
	grainPhaseFM = grainPhaseFM.wrap(0, 1);
	grainPhaseFM = Phasor.ar(DC.ar(0), grainFreq + (grainFreq * fmod) * SampleDur.ir);

	[
		grainPhasePM_A,
		grainPhasePM_B,
		grainPhaseFM
	];

}.plot(0.021);
)

// 1.5.3.) cascading transfer functions - "phase shaping" and "phase increment distortion"

(
var eventData = { |rate|

	var eventPhase, eventSlope, eventTrigger;

	eventPhase = VariableRamp.ar(rate);
	eventSlope = ~grainFunctions.helperFunctions[\rampToSlope].(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = ~grainFunctions.helperFunctions[\rampToTrig].(eventPhase);

	(
		phase: eventPhase,
		slope: eventSlope,
		trigger: eventTrigger
	);

};

{
    var tFreq, events;
    var subSampleOffset, accumulator;
    var windowSlope, windowPhase, grainWindow;
    var grainFreq, grainSlope, grainPhase;

    tFreq = \tFreq.kr(100);
    events = eventData.(tFreq);

	subSampleOffset = ~grainFunctions.helperFunctions[\subSampleOffset].(
		events[\phase],
		events[\slope],
		events[\trigger]
	);

	accumulator = ~grainFunctions.helperFunctions[\accumSubSample].(
		trig: events[\trigger],
		subSampleOffset: subSampleOffset
	);

    /////////////////////////////////////////////////////////////////////////

    windowSlope = Latch.ar(events[\slope] / max(0.001, \overlap.kr(1)), events[\trigger]);
    windowPhase = (windowSlope * accumulator).clip(0, 1);

    /////////////////////////////////////////////////////////////////////////

    grainFreq = \freq.kr(200);
    grainSlope = grainFreq * SampleDur.ir;

    /////////////////////////////////////////////////////////////////////////

    grainPhase = ~unitShapers.lerpingFunctions[\exponential].(windowPhase, \shapeA.kr(0.5));
    grainPhase = (grainPhase * Latch.ar(grainSlope / windowSlope, events[\trigger])).wrap(0, 1);

	grainPhase = grainPhase + (sin(windowPhase * pi) / pi * Latch.ar(grainSlope / windowSlope, events[\trigger]) * \index.kr(1));

	grainPhase = ~unitShapers.lerpingFunctions[\quinticSeat].(grainPhase, \shapeB.kr(0.25), \height.kr(0.5));
	//grainPhase = ~unitShapers.lerpingFunctions[\sigmoidToSeat].(grainPhase, \shapeC.kr(1));

	sin(grainPhase * 2pi);

}.plot(0.021);
)
5 Likes