«supercollider implementation of padsynth algorithm.» by 56228375
on 13 Jan'18 12:22 inAn implementation of Paul Nasca Octavian's excellent PadSynth algorithm in supercollider, driven from patterns. This code is accompanied by a blog article http://technogems.blogspot.be/2018/01/baking-sound-in-supercollider.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
( //sample creation using the PadSynth algorith described at http://wiki.linuxmusicians.com/doku.php?id=zynaddsubfx_manual#padsynth_algorithm //based on code from Donald Craig http://new-supercollider-mailing-lists-forums-use-these.2681727.n2.nabble.com/Epic-Pads-td7487382.html#a7492701 s.waitForBoot({ var table, re, im, tab, fftsize, result, pars, freqs, amps, bandwidth, partials, note; var samplerate = s.sampleRate; var sgroup; var fxgroup1,fxgroup2; var reverbbus,chorusbus; var reverb, chorus; var prepareSingleBuffer, prepareAllBuffers; var spectrum, xvals; s.freeAll; s.freeAllBuffers; s.sync; sgroup = Group.new; fxgroup1 = Group.after(sgroup); fxgroup2 = Group.after(fxgroup1); reverbbus = Bus.audio(s, 1); chorusbus = Bus.audio(s, 1); ~buffers = []; SynthDef(\padfilterenv, { | out=0, referencefreq=130, amp=0.5, freq=440, buffer=1, gate=1, attack=0.1, decay=0.2, sustain=0.6, release=2, filtercutoff=10000, filterresonance=1.0, filterattack=0.01, filterdecay=0.2, filtersustain=0.8, filterrelease=2, filtergain=1.0, glissando=0 | var env = EnvGen.ar(Env.adsr(attack, decay, sustain, release), gate, doneAction:Done.freeSelf); var env2 = EnvGen.ar(Env.adsr(filterattack, filterdecay, filtersustain, filterrelease), gate, doneAction:Done.none); var frequency = VarLag.ar(freq, glissando, warp:\exponential); var sig = env*PlayBuf.ar(1, buffer, rate:((frequency.cpsmidi)-(referencefreq.cpsmidi)).midiratio, loop:1); sig = RLPF.ar(sig, env2*filtercutoff, rq:filterresonance, mul:filtergain); Out.ar(out, amp*sig); }, rates:[nil, nil, nil, \ar, nil, nil, nil, nil, nil, nil, nil]).add; SynthDef(\reverb, { | out=0, inbus, mix=0.5, room=1.0 | var insig = FreeVerb.ar(In.ar(inbus, 1),mix,room); Out.ar(out, insig!2); }).add; SynthDef(\chorus, { | outbus=0, inbus, predelay=0.08, speed=0.05, depth=0.1, ph_diff=0.5 | var in, sig, modulators, numDelays = 12; in = In.ar(inbus, 1) * numDelays.reciprocal; modulators = Array.fill(numDelays, { |i| LFPar.kr(speed * rrand(0.94, 1.06), ph_diff * i, depth, predelay); }); sig = DelayC.ar(in, 0.5, modulators); sig = sig.sum; //Mix(sig); Out.ar(outbus, sig!2); // output in stereo }).add; s.sync; // calculate single wavetable prepareSingleBuffer = { | partials /* flat list of [partial idx, partial amplitude, partial idx, partial amplitude, ...]. Partials often are integers (or close to) */, min_length, /* min length of generated wave table in seconds */ spread /* band width used to generate new partials around the existing partials */, reference_note /* note for which this spectrum is being generated */| var fftsize = (min_length*s.sampleRate).nextPowerOfTwo; var pars = (partials.size/2); var bandwidth = (1+spread); var note = reference_note; var table = Signal.newClear(fftsize); var tab = Signal.fftCosTable(fftsize); var re = Signal.newClear(fftsize); var im = Signal.newClear(fftsize); var freqs = Array.newClear(pars); var amps = Array.newClear(pars); var buffer; var deinterlaced; fftsize.do({ |i| re[i] = 0.0; im[i] = 0.0; table[i] = 0.0; }); // partials are specified in a flat list containing // each time partial number followed by corresponding partial volume. // first deinterlace this flat list into a list of frequencies and a list of amplitudes deinterlaced = partials.unlace; freqs = deinterlaced[0]*(note.midicps); amps = deinterlaced[1]; // next we're going to generate extra (smeared) partials. This helps in adding life and warmth to the sounds. // if you specify spread == 0, no extra partials will be added pars.do({ |i| var freq, lo, hi,amp; freq = freqs[i]; amp = amps[i]; lo = ((freq/bandwidth)*(fftsize/samplerate)).round; // partial at frequency freq will be smeared over frequencies lo to hi hi = ((freq*bandwidth)*(fftsize/samplerate)).round; // generate extra partials between frequencies lo = freq/(1+spread) and hi = freq*(1+spread) (hi-lo+1).do({ |j| var mag, phase, val; var index = j.linlin(0, hi-lo, lo, hi); // only fill up lower half of spectrum: // right half later is derived from this left half to ensure a real-valued inverse fourier transform if(index < (fftsize/2), { if ((hi == lo), { mag = amp; table[index] = table[index] + mag; // add it to the result table }, /* else */ { val = j.linlin(0, hi-lo, -1, 1); mag = exp(val*val*10.0.neg) * amp; // generates a bell-shaped curve for val in [-1,1] with y-values between [-amp, amp] table[index] = table[index] + mag; // add it to the result table to create a "smeared" partial }); phase = rrand(-pi, pi); // set random phase re[index] = re[index] + (cos(phase)*mag); im[index] = im[index] + (sin(phase)*mag); }); }); }); // at this point, table contains the sum of all the specified + extra generated partials // calculate right half of spectrum to get a real-valued inverse FFT // right half must be the mirrored complex conjugate (i.e. make imaginary part negative) of the left half (fftsize/2-1).do({ | i | re[i+(fftsize/2)] = re[(fftsize/2)-i]; im[i+(fftsize/2)] = im[(fftsize/2)-i].neg; }); // inverse fourier transformation: resulting imaginary part should be (very close to) all zeros re = ifft(re, im, tab); // re.real.normalize scales the result so it falls between 0 and 1. // Next, make sure to normalize the maximum volume to -3dB. result = re.real.normalize * ((-3).dbamp); // load the result in a buffer buffer = Buffer.loadCollection(s, result); // and return the buffer as result of the function buffer; }; // calculate 2 wavetables per octave prepareAllBuffers = { | partials = #[ 1.01, 0.1722, 2.00, 0.0056, 2.99, 0.1609, 3.99, 0.0333, 5.00, 0.1157, 5.99, 0.1149, 6.98, 0.0079, 7.98, 0.0620, 8.99, 0.0601, 9.99, 0.0104, 10.98, 0.0134, 11.97, 0.0122, 12.99, 0.0058, 13.98, 0.0110, 14.98, 0.0029, 15.97, 0.0045, 16.98, 0.0023, 17.98, 0.0010, 18.97, 0.0016, 19.96, 0.0021, 20.96, 0.0008, 21.97, 0.0021, 22.96, 0.0001, 23.96, 0.0012, 24.95, 0.0003, 25.97, 0.0002, 26.96, 0.0003, 27.95, 0.0002, 30.96, 0.0002, 32.94, 0.0002, 34.96, 0.0001, 35.95, 0.0002, 37.93, 0.0001 ], min_length=5, spread = 0.1 | var buffers = []; var maxOctaves = 8; // prepare two wavetables per octave (0,1..8).do({ | octave | var reference_note_1 = (octave*12); var reference_note_2 = (octave*12) + 6; buffers = buffers.add(prepareSingleBuffer.value(partials, min_length, spread, reference_note_1)); buffers = buffers.add(prepareSingleBuffer.value(partials, min_length, spread, reference_note_2)); }); buffers; }; // calculate a desired spectrum (note: envelopes and filters/resonators/fx are just as important in determining overall experience) xvals = (1,2..33).as(Array); spectrum = Signal.newClear(33).waveFill({ | x, old, idx | var lookup; lookup = [ 14.2, 8.8, 7.3, 8, 5.7, 7, 6.8, 5.8, 8.7, 6.9, 3.2, 2.1, 4, 3, 1.8, 1.1, 2.5, 1.5]; // cello-esque if attack 0.3, no filter env, small reverb if ((idx < lookup.size), {lookup[idx]}, {0}); }, start:1, end:0).as(Array); spectrum = [xvals, spectrum].lace; // prepare the wavetables using inverse FFT ~buffers = prepareAllBuffers.value(spectrum, 5 /* minimum length of buffer in seconds */, 0.03 /* spread of partials during detuning */); s.sync; // start the fx synths reverb = Synth(\reverb, [ \out, 0, \inbus, reverbbus, \mix, 0.1, \room, 0.5, ], target:fxgroup2); chorus = Synth(\chorus, [ \out, reverbbus, \inbus, chorusbus ], target:fxgroup1); // create a composition // Pbind a new synth for each note. p = Pbind( \instrument, \padfilterenv, \out, reverbbus, \mynote, Pseq([Pseq((40,41..72), 1), Prand((48,49..72), 100)], inf), \myreference, (Pkey(\mynote)-(Pkey(\mynote)%6)), \referencefreq, Pkey(\myreference).midicps, \freq, Pkey(\mynote).midicps, \dur, Pseq([Pseq([0.5], 72-24), Prand((0.1,0.2..1.0), 100)], inf), \amp, 0.7, \buffer, Pfunc({ |ev| ~buffers[(ev[\myreference]/6).round(1)].bufnum; }), \attack, 0.3, \decay, 0.1, \sustain, 0.9, \release, 1.0, \filtergain, 0.3, \filtercutoff, 1000, \filterattack, 0.01, \filterdecay, 0.1, \filtersustain, 1.0, \filterrelease, 0.3, \filterresonance, 1.0, \glissando, 0, \vibratofreq, 3.0, \vibratodepth, 0.015, \group, sgroup); // Pmono creates only one synth, and updates its parameters. This allows e.g. for glissando's. q = Pmono( \padfilterenv, \out, reverbbus, \mynote, Pseq([40, 52], inf), \myreference, (Pkey(\mynote)-(Pkey(\mynote)%6)), \referencefreq, Pkey(\myreference).midicps, \freq, Pkey(\mynote).midicps, \dur, Pseq([2], inf), \amp, 0.7, \buffer, Pfunc({ |ev| ~buffers[(ev[\myreference]/6).round(1)].bufnum; }), \attack, 0.3, \decay, 0.1, \sustain, 0.9, \release, 1.0, \filtergain, 0.3, \filtercutoff, 1000, \filterattack, 0.01, \filterdecay, 0.1, \filtersustain, 1.0, \filterrelease, 0.3, \filterresonance, 1.0, \glissando, 0.5, \vibratofreq, 3.0, \vibratodepth, 0.015, \group, sgroup); c = Ppar([p,q], inf); c.play; }); )
reception
comments