// title: Paulstretch for SuperCollider // author: jpdrecourt // description: // This is a port of the basic Paulstretch algorithm to SuperCollider (no onset detection). Mono version, for stereo, use 2 instances hard panned. The sound buffer needs to be mono too, so use Buffer.readChannel to extract separate channels. // The stretch parameter is modulatable. That allows for phasing effects if using more than one instance. // // Thanks to Paul for his feedback! // Check his work at http://www.paulnasca.com/ // code: ( SynthDef(\paulstretchMono, { |out = 0, bufnum, envBufnum, pan = 0, stretch = 50, window = 0.25, amp = 1| // Paulstretch for SuperCollider // Based on the Paul's Extreme Sound Stretch algorithm by Nasca Octavian PAUL // https://github.com/paulnasca/paulstretch_python/blob/master/paulstretch_steps.png // // By Jean-Philippe Drecourt // http://drecourt.com // April 2020 // // Arguments: // out: output bus (stereo output) // bufnum: the sound buffer. Must be Mono. (Use 2 instances with Buffer.readChannel for stereo) // envBufnum: The grain envelope buffer created as follows: //// envBuf = Buffer.alloc(s, s.sampleRate, 1); //// envSignal = Signal.newClear(s.sampleRate).waveFill({|x| (1 - x.pow(2)).pow(1.25)}, -1.0, 1.0); //// envBuf.loadCollection(envSignal); // pan: Equal power panning, useful for stereo use. // stretch: stretch factor (modulatable) // window: the suggested grain size, will be resized to closest fft window size // amp: amplification var trigPeriod, sig, chain, trig, pos, fftSize; // Calculating fft buffer size according to suggested window size fftSize = 2**floor(log2(window*SampleRate.ir)); // Grain parameters // The grain is the exact length of the FFT window trigPeriod = fftSize/SampleRate.ir; trig = Impulse.ar(1/trigPeriod); pos = Demand.ar(trig, 0, demandUGens: Dseries(0, trigPeriod/stretch)); // Extraction of 2 consecutive grains // Both grains need to be treated together for superposition afterwards sig = [GrainBuf.ar(1, trig, trigPeriod, bufnum, 1, pos, envbufnum: envBufnum), GrainBuf.ar(1, trig, trigPeriod, bufnum, 1, pos + (trigPeriod/(2*stretch)), envbufnum: envBufnum)]*amp; // FFT magic sig = sig.collect({ |item, i| chain = FFT(LocalBuf(fftSize), item, hop: 1.0, wintype: -1); // PV_Diffuser is only active if its trigger is 1 // And it needs to be reset for each grain to get the smooth envelope chain = PV_Diffuser(chain, 1 - trig); item = IFFT(chain, wintype: -1); }); // Reapply the grain envelope because the FFT phase randomization removes it sig = sig*PlayBuf.ar(1, envBufnum, 1/(trigPeriod), loop:1); // Delay second grain by half a grain length for superposition sig[1] = DelayC.ar(sig[1], trigPeriod/2, trigPeriod/2); // Panned output Out.ar(out, Pan2.ar(Mix.new(sig), pan)); }).add; ) // Example ({ var envBuf, envSignal, buffer; buffer = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); // The grain envelope envBuf = Buffer.alloc(s, s.sampleRate, 1); envSignal = Signal.newClear(s.sampleRate).waveFill({|x| (1 - x.pow(2)).pow(1.25)}, -1.0, 1.0); envBuf.loadCollection(envSignal); s.sync(); // Runs indefinitely Synth(\paulstretchMono, [\bufnum, buffer.bufnum, \envBufnum, envBuf.bufnum]); }.fork; )