Additive Synthesis

You can modulate every parameter in the padSynthDistribution function though, including the width of the distribution - this gives enough control imo. Again, I did it this way so I could use it with your additive synthesis functions, if you look at the refactor above it is actually written using a similar method to what you were doing, I am simply assigning the indices for the do loops in the ‘constructor’ - sounds great with the comb filter :wink:

Adding bwi = 1 / (chain[\harmonicIdx]) * bwScale; could be a better way to parameterise the amplitude of the sidebands too

I guess there are three ways of thinking:

1.) initialise an array of harmonics (integer ratios) based on a number of partials
2.) pass an array of predefined harmonics from the language to the server
3.) create sidebands with the formant attempt using singleSideband modulation or modFM

i guess the padSynthDistribution attempt is inbetween those three things.

1 Like

do you have screenshots of the reaktor patches of razor (i hope we dont need a single-sample feedback loop, fingers crossed) ? Then we could work together on the reverb implementations. thats a cool idea!

Ive reworked the linear comb filter, now combDensity = 1 will filter out every other partial resulting in a square wave and the combWarp param will warp the comb response, so that the nodges are more dense in the lower register (like neck pickup) or more dense in the higher register (like bridge pickup). if you open the freqscope and choose linear mode you can see that quite nicely. When combWarp = 0 (logarithmic), its more or less equal to the exponential comb filter based on log2 but with the morphing capability. The former addHarmonicClustering function is now directly imbedded in the comb filter.

combWarp = 0.5 (linear)

combWarp = 0 (logarithmic)

combWarp = 1 (exponential)

var coreQuintic = { |x|
	x * x * x * x * x;
};

var outQuintic = { |x|
	1 - coreQuintic.(1 - x);
};

var quinticOutToLinear = { |x, shape|
	var mix = shape * 2;
	var easeOut = outQuintic.(x);
	easeOut * (1 - mix) + (x * mix);
};

var linearToQuinticIn = { |x, shape|
	var mix = (shape - 0.5) * 2;
	var easeIn = coreQuintic.(x);
	x * (1 - mix) + (easeIn * mix);
};

var expToLinearMorph = { |x, shape|
	Select.kr(shape > 0.5, [
		quinticOutToLinear.(x, shape),
		linearToQuinticIn.(x, shape)
	]);
};

var addCombFilter = { |chain, combDensity, combWarp, combPeak|
    var ratios = chain[\freqs] / chain[\freq];  // get harmonic ratios
    var normalizedRatios = (ratios / ratios[chain[\numPartials] - 1]).clip(0, 1);
    var warpedRatios = expToLinearMorph.(normalizedRatios, combWarp);  // apply warping
    var rescaledRatios = warpedRatios * ratios[chain[\numPartials] - 1];  // rescale to original range
	var phase = (rescaledRatios * (combDensity * 0.5)).wrap(0, 1);
    chain[\amps] = chain[\amps] * raisedCos.(phase, combPeak);
    chain;
};
3 Likes

to save some ugens you can implement the raisedCosine window with:

var raisedCos = { |phase, index|
    var cosine = cos(phase * 2pi);
    var raised = exp(index.abs * (cosine - 1));
	var hanning = 0.5 * (cosine + 1);
    raised * hanning;
};

instead of:

    var raisedCos = { |phase, index|
        var cosine = cos(phase * 2pi);
        var raised = exp(index.abs * (cosine - 1));
        var hanning = 0.5 * (1 + cos(phase * 2pi));
        raised * hanning;
    };

There are also some sin/cos approximations which would probably be well suited here in terms of CPU.

1 Like

The former transfer function to skew the comb nodges themselves will find its way in an additional spectral granulation function im currently working on.

2 Likes

Cant remember the additive comb filter approach ive used for this test recording, but i still love the sounds:

5 Likes

hi. can you share the examples of them? I wonder how they’re implemented by you

This is super hawt sounding. Please post more hawt clips.

1 Like

These implementations have been developed by @nathan during some lessons i took with him about 2-3 years ago. I made the comment to say that im not interested in other additive filters which you could find in razor, because i already have these covered.
This thread is already packed with stuff i have developed or refined over the last years. Im normally more then generous in sharing stuff i have been working on for countless hours, but i would rather keep these for myself.
But would be happy to discuss some of your attempts.

I got what you meant in the comment right away. It’s ok if you don’t want to share

Here’s my go at porting a bunch of the Reaktor Razor operators to SC. I’ve translated the functions that I thought looked most interesting / readable. The Reaktor patch is quite difficult to decipher so I’m not 100% on all of it. I had particular trouble understanding how the reverb works, so I gave up on it (plus it uses z-1 sample history). Some of this is not particularly sc idiomatic because I translated one to one best I could from the Reaktor patcher. I also couldn’t quite work out if I am chaining the dissonance operators properly, not entirely sure the freqs are being scaled correctly.

The patch requires LFBrownNoise also.

I might do some of the waveforms next or try and find another way to imitate some of the reverbs.

(
var initChain = { |numPartials, freq|
    (
        freq: freq,
        numPartials: numPartials,
        ratios: (1..numPartials),
        amps: 1 ! numPartials,
    )
};

var addLimiter = { |chain|
    var nyquist = SampleRate.ir * 0.5;
    var fadeStart = nyquist - 2000;
    var limiter = (1 - (chain[\freqs] - fadeStart) / 1000).clip(0, 1);
    chain[\amps] = chain[\amps] * limiter;
    chain;
};

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

// RAZOR Dissonance
var partialDetune = {|chain, amount=0.1, partial_select=2, mode=0|
    var idx = chain[\ratios] - 1;
    var freq = chain[\freq];
    var detune = ((50 / freq) * amount) - mode;
    var whichIdx = ((idx / partial_select).floor * partial_select);
    var isDetuned = (idx - whichIdx).clip(0,1);
    detune = detune * isDetuned;
    // detune = (chain[\freq] * (chain[\ratios] + detune));
    detune = (chain[\freqs] ? 0) + (chain[\freq] * detune);
    chain[\freqs] = detune;
    chain;
};

//not sure about this one
var stiffString = {|chain, amount=0.5|
    var scaledAmt = amount.linlin(0, 1, -140, 60);
    var db2AF = scaledAmt.dbamp;
    var idx = chain[\ratios];
    var newRatio = (idx.squared * db2AF) + 1;
    newRatio = ((log2(newRatio) * 0.5) ** 2) * idx + 1;

    //unsure what this should be, unclear in the reaktor patch
    // chain[\freqs] = newRatio * chain[\freq];
    chain[\freqs] = (chain[\freqs] ? 0) + (chain[\freq] * newRatio);
    chain;
};

var centroid = {|chain, amount, targetFreq|
    var newRatio;
    var partialPitch = chain[\freqs];
    var pitchArr = targetFreq ! chain[\numPartials];
    newRatio = amount.linlin(0, 1, partialPitch, pitchArr);    
    chain[\freqs] = newRatio;
    chain;
};

//not sure about this one, too many magic numbers
var reverse = {|chain, amount|
    var newRatio;
    amount = 1 - amount;
    newRatio = amount.linlin(0, 1, chain[\ratios].ratiomidi, (321.ratiomidi - 320.ratiomidi) ! chain[\numPartials]).min(200).midiratio;
    newRatio = newRatio * chain[\freqs];
    chain[\freqs] = newRatio;
    //TODO amps
    chain;
};

//RAZOR Stereo
//the same(?) as pan2.ar(sig, line).sum but for the sake of completeness:
var panLaw = {|in, pos|
    var left = in * (4 - (1 - pos)) * (1 - pos) * 0.333333;
    var right = in * (4 - (1 + pos)) * (1 + pos) * 0.333333;
    [left, right];
};

//in all examples ramp is the normalised selection of 0-n partial indices 
var simplePan = {|sig, chain, amount, ramp|
    var partialPitch, pitch, start, length, end;
    var rampIndices, centerIndices;
    var x0, y0, m0, x, line;

    partialPitch = chain[\freqs].cpsmidi;
    pitch = chain[\freq].cpsmidi;
    start = pitch;
    length = ramp.linlin(0, 1, chain[\freqs][0], chain[\freqs][chain[\numPartials]-1]).cpsmidi;
    end = length;
    
    rampIndices = 1 - (partialPitch.floor / end.floor).floor.clip(0,1);
    centerIndices = (0.5 - rampIndices).clip(0,1);

    x0 = end; y0 = 1; m0 = 1/length.max(12); x=partialPitch;
    line = ((x - x0 * m0 + y0) * rampIndices * amount) + centerIndices;
    line = line.linlin(0,1,-1,1).lag(0.1, 0.1);
    panLaw.(sig, line);
};

var autoPan = {|sig, chain, amount, ramp, saw, cycles|
    var partialPitch, partialPitchScaled, pitch, start, length, end;
    var rampIndices, centerIndices;
    var x0, y0, m0, x, line;
    //scaling
    partialPitch = chain[\freqs].cpsmidi;
    partialPitchScaled = partialPitch - 80;
    pitch = chain[\freq].cpsmidi;
    cycles = ((1-cycles)*30.neg).dbamp * cycles;
    partialPitchScaled = partialPitchScaled * cycles;
    saw = saw * 0.5.neg;
    partialPitchScaled = partialPitchScaled + saw;
    partialPitchScaled = partialPitchScaled - partialPitchScaled.collect(_.round);
    partialPitchScaled = partialPitchScaled * (8 - (partialPitchScaled.abs * 16));
    //panned indices
    start = pitch;
    length = ramp.linlin(0, 1, chain[\freqs][0], chain[\freqs][chain[\numPartials]-1]).cpsmidi;
    end = length;
    rampIndices = 1 - (partialPitch.floor / end).floor.clip(0,1);
    centerIndices = (1 - rampIndices);
    //ramp
    x0 = end; y0 = amount; m0 = 1/length.max(12); x=partialPitch;
    line = ((x - x0 * m0 + y0) * rampIndices) + centerIndices;
    line = line * partialPitchScaled;
    line = line.lag(0.1, 0.1);
    panLaw.(sig, line);
};

var stereoSpread = {|sig, chain, amount, ramp, saw, cycles|
    var idx, partialPitch, partialPitchScaled, pitch, start, length, end;
    var rampIndices, centerIndices, spreadIndices;
    var x0, y0, m0, x, line;

    idx = chain[\ratios];
    partialPitch = chain[\freqs].cpsmidi;
    partialPitchScaled = partialPitch - 80;
    pitch = chain[\freq].cpsmidi;
    cycles = ((1-cycles)*30.neg).dbamp * cycles;
    partialPitchScaled = partialPitchScaled * cycles;
    saw = saw * 0.5.neg;
    partialPitchScaled = partialPitchScaled + saw;

    partialPitchScaled = partialPitchScaled - partialPitchScaled.collect(_.round);
    partialPitchScaled = partialPitchScaled * (8 - (partialPitchScaled.abs * 16));
    
    start = pitch;
    length = ramp.linlin(0, 1, chain[\freqs][0], chain[\freqs][chain[\numPartials]-1]).cpsmidi;
    end = length;
    rampIndices = 1 - (partialPitch.floor / end).floor.clip(0,1);
    centerIndices = (1 - rampIndices);

    //flip polarity of every second partial
    spreadIndices= idx.wrap(0,1).linlin(0,1,-1,1);
    partialPitchScaled = partialPitchScaled * spreadIndices;

    x0 = end; y0 = amount; m0 = 1/length.max(12); x=partialPitch;
    line = ((x - x0 * m0 + y0) * rampIndices) + centerIndices;
    line = line * partialPitchScaled;
    panLaw.(sig, line);
};

var air = {|sig, chain, amount, speed, min, max|

    var idx = chain[\ratios];
    var partialFreqs = chain[\freqs];
    var noise_L, noise_R;
    var idxAmp, rangeAmp, ampScale;
    var amtFade, amtMult;
    var pan, left, right;

    speed = speed.linlin(0,1,-200,0).midiratio;
    min = min.linlin(0,1,-70,40);
    max = max.linlin(0,1,-70,40);
    
    noise_L = partialFreqs * speed + 0.333;
    noise_R = partialFreqs * speed + 0.456;

    noise_L = LFBrownNoise2.ar(noise_L, 1, 1, 5);
    noise_R = LFBrownNoise2.ar(noise_R, 1, 1, 5);

    idxAmp = (chain[\amps] * (idx - 1)).max(0.0001).dbamp - max;
    rangeAmp = 1/(max - min).max(0.1).neg;

    ampScale = (idxAmp * rangeAmp).clip(0, 1) * amount;

    amtFade = (1 - ampScale);
    amtFade = (2 - amtFade) * amtFade;
    amtMult = (2 - ampScale) * ampScale;
    amtMult = amtMult * 2;

    noise_L = (noise_L * amtMult) + amtFade;
    noise_R = (noise_R * amtMult) + amtFade;
    [noise_L * sig, noise_R * sig];
};

//notch
var notchFilter = { |chain, notchFreq, notchWidth|
    var freqs = chain[\freqs];
    var amps = chain[\amps];
    
    var dist = (freqs - notchFreq).abs;
    var attenuation = 1.0 - exp(dist.neg / notchWidth);
    
    chain[\amps] = amps * attenuation;
    chain;
};

{
    var chain, sig;
    chain = initChain.(60, 80);
    chain = makeStretchedHarmonicSeries.(chain, 0);

    //use one of these
    chain = partialDetune.(chain, amount: 0.1, partial_select: 2, mode: 0);
    // chain = stiffString.(chain, amount: 0.5);
    // chain = centroid.(chain, amount: SinOsc.ar(0.05).unipolar, targetFreq: 500);
    // chain = reverse.(chain, amount: SinOsc.ar(0.05).unipolar);
    chain = notchFilter.(chain, SinOsc.ar(0.5).linlin(-1,1,50,1000), 1000);
    chain = addLimiter.(chain);

    sig = SinOsc.ar(
        freq: chain[\freqs],
        phase: ({ Rand(0, 2pi) } ! chain[\numPartials]),
        mul: chain[\amps]
    );
    
    //use one of these
    
    //simple
    // sig = simplePan.(sig, chain, amount: SinOsc.ar(1).unipolar, ramp: 1);
    
    //these two are quite similar
    // sig = autoPan.(sig, chain, amount: SinOsc.ar(1).unipolar, ramp: SinOsc.ar(3).unipolar , saw: LFSaw.ar(0.5).unipolar, cycles: SinOsc.ar(0.1).unipolar);
    sig = stereoSpread.(sig, chain, amount: SinOsc.ar(1).unipolar, ramp: 1, saw: LFSaw.ar(0.5).unipolar, cycles: 1);
    
    //the best one...
    // sig = air.(sig, chain, amount: 0.5, speed: 0.8, min: 0.1, max: 0.9);

    sig = sig * -30.dbamp;
}.play;
)
2 Likes

Hey, good Job. Have looked at the detuning algos and the notch filter today. The stiffString is pretty similiar to the detuning we already got, which makes sense because it’s also based on a string Model. Will have a closer Look After the holidays and try to find the corresponding paper on my Hard Drive. The notch Filter implementation is similar to the gaussian window.

The inharmonicity formula we already got is backed up by this paper: https://www.researchgate.net/publication/228587669_Audibility_of_Inharmonicity_in_String_Instrument_Sounds_and_Implications_to_Digital_Sound_Synthesis

Im not sure where the Razor formula for string stiffness is derived from, seems empirical based on try and error and the db range arbitrary.

Yeah I’m not a huge fan of the sound of the RAZOR stiff string module on second thoughts, even in the original reaktor patch. No idea what errorsmith would have based the modules on - the synth is very old at this point so it is hard to know.

Im going through the different functions one by one and figure out what to keep, what to adjust etc.

wow love this sounds

oh, this is pretty fun - you can extract sines freq/mag realtime w FluidSineFeature and feed them into all the functions.

var analyseInput = {|chain, buf, freqLag, ampLag, order|
    var source = PlayBuf.ar(1, buf, loop: 1);
    
    var analysis = FluidSineFeature.kr(
        source, 
        order: order,
        numPeaks: chain[\numPartials], 
        maxNumPeaks: 50, 
        windowSize: 512, 
        fftSize: 4096
    );
    
    chain[\freqs] = analysis[0].lag(freqLag, freqLag);
    chain[\amps] = analysis[1].lag(ampLag, ampLag);
    chain;
};

etc..

chain = initChain.(50, 440);
chain = analyseInput.(chain, buf, freqLag: 0.01, ampLag: 0.5, order: 1);

etc...

1 Like
(
    ~extractSines_smooth = {|chain, sig, freqLag, ampLag, order, transpose, winSize=512, fftSize=4096, hopSize=4, thresh=0|

        var analysis, freqs, amps;
        
        analysis = FluidSineFeature.kr(
            sig, 
            order: order,
            numPeaks: chain[\numPartials], 
            maxNumPeaks: 50, 
            windowSize: winSize, 
            fftSize: fftSize,
            hopSize: hopSize,
        );
        
        transpose = transpose.midiratio;
    
        amps = analysis[1].lag(ampLag, ampLag);
        freqs = Latch.ar(analysis[0], amps > thresh).lag(freqLag, freqLag) + 0.00001 * transpose;
    
        chain[\freqs] = freqs;
        chain[\amps] = amps;
        chain;
    };
    
    ~quantizePartials = {|chain, scale, strength=1, baseFreq=(60.midicps)|
        var freq = chain[\freqs];
        var ratios = scale.ratios;
        var buf = ratios.as(LocalBuf);
        var octave = (freq/baseFreq).log2.floor;
        var position = IndexInBetween.kr(buf, (freq/baseFreq * (2 ** octave.neg)));
        var scaleDegree = position.round;
        var quantizedFreq = baseFreq * Index.kr(buf, scaleDegree) * (2 ** octave);
    
        var distance = (position - scaleDegree).abs;
        var scaledStrength = strength * (1 - distance);
        var outFreq = (quantizedFreq * scaledStrength) + (freq * (1-scaledStrength));
    
        chain[\freqs] = outFreq;
        chain;
    };

    Ndef(\tracking, {
        var sig, chain, file, buf, src, follower;
        var ampDrift;

        var fbIn;
        
        file = "your file";
        buf = Buffer.readChannel(s, file, channels: [0]);
        
        //get fb
        fbIn = LocalIn.ar(2) * \feedback.kr(0);
        src = PlayBuf.ar(1, buf, loop: 1) + fbIn;
            
        //freq does nothing here
        chain = ~initChain.(numPartials: 50, freq: 440);
        //order 0 or 1
        chain = ~extractSines_smooth.(
            chain, 
            src, 
            freqLag: 0.1, 
            ampLag: 1, 
            order: 0, 
            transpose: -24, 
            winSize: 512, 
            fftSize: 1024, 
            hopSize: -1, 
            thresh: 0.001
        );

        chain = ~quantizePartials.(chain, scale: Scale.major, strength: 1);
        chain = ~addLimiter.(chain);
        
        sig = SinOsc.ar(
            freq: chain[\freqs],
            phase: ({ Rand(0, 2pi) } ! chain[\numPartials]) * SinOsc.ar(0.1).unipolar,
            mul: chain[\amps]
        );
        
        sig = sig[0,2..].sum + ([-1,1] * sig[1,3..].sum);
        
        sig = Compander.ar(sig, sig,
            thresh: 0.5,
            slopeBelow: 1,
            slopeAbove: 0.1,
            clampTime:  0.01,
            relaxTime:  0.01
        );
        
        sig=sig.softclip;
        sig = sig.sanitize;

        //send fb
        LocalOut.ar(sig.sanitize);
        
        sig = sig * 0.dbamp;
        sig;
    }).play;
)

Follow and tune input partials to scale. Cool vocodoer effect, sounds a lot like a resonator bank that is more adaptive to pitch content, a lot to explore here I think.

5 Likes

currently working on an additional warping feature for the comb filter :slight_smile:

5 Likes