From c29be29194d22deeb1dc5edad1ec3e217fb224f5 Mon Sep 17 00:00:00 2001 From: Leo Coogan Date: Tue, 15 Apr 2025 03:11:10 -0400 Subject: [PATCH] experiments & examples --- eli_fieldsteel_examples/vocoder.scd | 26 +++++ gated reverb.scd | 20 ++++ nathan_ho_examples/pulsar.scd | 165 ++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 eli_fieldsteel_examples/vocoder.scd create mode 100644 gated reverb.scd create mode 100644 nathan_ho_examples/pulsar.scd diff --git a/eli_fieldsteel_examples/vocoder.scd b/eli_fieldsteel_examples/vocoder.scd new file mode 100644 index 0000000..9193014 --- /dev/null +++ b/eli_fieldsteel_examples/vocoder.scd @@ -0,0 +1,26 @@ +b = Buffer.read(s, "/home/lcoogan/snd/releases/desolation-mountain/02 - crawling and quietly seething was the figure of the trees.wav"); + + +( +SynthDef(\vocoder, { + + var mod, car, bpfmod, num = 30, track, bpfcar, + bpfhz = (1..num).linexp(1, num, 25, 16000), + q = \q.kr(20); + + // create modulator & carrier + mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); + car = CombC.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); + + // track spectrum of modulator + bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); + track = Amplitude.kr(bpfmod).lag3(0.03) * 2; + + // apply spectrum to carrier + bpfcar = BPF.ar(car, bpfhz, 1/q, q.sqrt) * track; + + Out.ar(0, Splay.ar(bpfcar.scramble, spread: 0.1)); +}).play; +) + +x.set(\freq, rrand(30, 50).midicps); \ No newline at end of file diff --git a/gated reverb.scd b/gated reverb.scd new file mode 100644 index 0000000..2a21588 --- /dev/null +++ b/gated reverb.scd @@ -0,0 +1,20 @@ +( +SynthDef(\gatedReverb, { + var dry, wet, reverbEnv; + + // Dry signal + dry = SinOsc.ar(440) * EnvGen.ar(Env.perc(0.01, 0.1), doneAction: 0); + + // Wet signal: full reverb + wet = FreeVerb.ar(dry.dup, mix: 1, room: 0.9, damp: 0.4); + + // Gating envelope for the reverb ONLY + reverbEnv = EnvGen.ar(Env.linen(0, 0.2, 0.001), doneAction: 2); // <- Cutoff happens here + + // Send dry directly, and apply the envelope to the wet + Out.ar(0, (dry + (wet * reverbEnv)) * 0.5); +}).add; +) + +Synth(\gatedReverb); + diff --git a/nathan_ho_examples/pulsar.scd b/nathan_ho_examples/pulsar.scd new file mode 100644 index 0000000..b217101 --- /dev/null +++ b/nathan_ho_examples/pulsar.scd @@ -0,0 +1,165 @@ +// https://nathan.ho.name/posts/pulsar-synthesis/ + +( +{ + var snd, freq, formantFreq, pulsaretPhase; + freq = 100; + formantFreq = 400; + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + pulsaretPhase.dup; +}.play; +) + +pulsaretPhase is then used in two ways. First, if it’s greater than or equal to 1, the pulsaret has stopped playing and silence is produced. Second, the pulsaretPhase indexes into the pulsaret signal. Here’s a sine wave pulsaret with proper gating: + +( +{ + var snd, freq, formantFreq, pulsaretPhase; + freq = 100; + formantFreq = 400; + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + snd = sin(pulsaretPhase * 2pi) * (pulsaretPhase < 1); + snd.dup; +}.play; +) + +We can generalize to multiple concatenated sine waves by multiplying the term inside the sin(...) by a value we’ll call sineCycles: + +( +{ + var snd, freq, formantFreq, sineCycles, pulsaretType, pulsaretPhase; + freq = 100; + formantFreq = 400; + sineCycles = 4; + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * (pulsaretPhase < 1); + snd.dup; +}.play; +) + +We can apply an approximately exponentially decaying window to the pulsaret, as opposed to the rectangular one we’ve been using so far: + +( +{ + var snd, freq, formantFreq, sineCycles, pulsaretPhase, window; + freq = 100; + formantFreq = 200; + sineCycles = 4; + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + window = pulsaretPhase.lincurve(0, 1, 1, 0, -4); + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1); + snd.dup; +}.play; +) + +That’s enough plotting. Let’s hear it with random modulation of all the parameters we discussed, including amplitude and pan position: + +( +{ + var snd, freq, formantFreq, sineCycles, pulsaretPhase, window, randomLFO; + randomLFO = { LFNoise2.kr(5) }; + freq = randomLFO.().linexp(-1, 1, 1, 1000); + formantFreq = randomLFO.linexp(-1, 1, 2, 8000); + + sineCycles = randomLFO.().linlin(-1, 1, 1, 4); + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + window = pulsaretPhase.lincurve(0, 1, 1, 0, -4); + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1); + snd = snd * randomLFO.().linlin(-1, 1, 0, 1); + snd = Pan2.ar(snd, randomLFO.() * 0.4); + snd = snd * -5.dbamp; + snd ! 2; +}.play(fadeTime: 0); +) + +As discussed in Microsound, let’s use three separate formants and pan them around the stereo field separately: + +( +{ + var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs; + formantCount = 3; + randomLFO = { LFNoise2.kr(5) }; + randomLFOs = { { randomLFO.() } ! formantCount }; + freq = randomLFO.().linexp(-1, 1, 1, 1000); + formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000); + sineCycles = randomLFOs.().linlin(-1, 1, 1, 4); + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + window = pulsaretPhase.lincurve(0, 1, 1, 0, -4); + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1); + snd = snd * randomLFOs.().linlin(-1, 1, 0, 1); + snd = Pan2.ar(snd, randomLFOs.() * 0.4); + snd = snd.flop.sum; + snd = snd * -2.dbamp; + snd; +}.play(fadeTime: 0); +) + +This gets old fast, and the shapes of the random LFOs are the culprit. They are all constantly moving smoothly and there’s a lack of space to make movement sound special, leading to monotony. Recalling the switching method that I talked about in my last blog post, let’s have each random LFO produce a random static value every now and then. Also, let’s modulate the rate of all LFOs with another global slow LFO that determines their overall rate: + +( +{ + var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs, lfoRate; + formantCount = 3; + lfoRate = LFDNoise1.kr(0.3).linexp(-1, 1, 0.1, 16); + randomLFO = { + var trigger; + trigger = Dust.kr(lfoRate); + Select.kr(ToggleFF.kr(trigger), [ + LFNoise2.kr(lfoRate), + TRand.kr(-1, 1, trigger) + ]); + }; + randomLFOs = { { randomLFO.() } ! formantCount }; + freq = randomLFO.().linexp(-1, 1, 1, 1000); + formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000); + sineCycles = randomLFOs.().linlin(-1, 1, 1, 4); + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + window = pulsaretPhase.lincurve(0, 1, 1, 0, -4); + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1); + snd = snd * randomLFOs.().linlin(-1, 1, 0, 1); + snd = Pan2.ar(snd, randomLFOs.() * 0.4); + snd = snd.flop.sum; + snd = snd * -2.dbamp; + snd; +}.play(fadeTime: 0); +) + +As always, it’s not just the synthesis recipe, it’s also how you modulate it. + +To bring this patch into the 21st century, we can stack on a few effects. Here are three pitch shifters (not just any pitch shifter, but the weird and metallic PitchShift) and three frequency shifters, all modulated randomly: + +( +{ + var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs, lfoRate; + formantCount = 3; + lfoRate = LFDNoise1.kr(0.3).linexp(-1, 1, 0.1, 16); + randomLFO = { + var trigger; + trigger = Dust.kr(lfoRate); + Select.kr(ToggleFF.kr(trigger), [ + LFNoise2.kr(lfoRate), + TRand.kr(-1, 1, trigger) + ]); + }; + randomLFOs = { { randomLFO.() } ! formantCount }; + freq = randomLFO.().linexp(-1, 1, 1, 1000); + formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000); + sineCycles = randomLFOs.().linlin(-1, 1, 1, 4); + pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq; + window = pulsaretPhase.lincurve(0, 1, 1, 0, -4); + snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1); + snd = snd * randomLFOs.().linlin(-1, 1, 0, 1); + snd = Pan2.ar(snd, randomLFOs.() * 0.4); + snd = snd.flop.sum; + [0.2, 0.1, 0.05].do { |windowSize| + snd = PitchShift.ar(snd, windowSize, randomLFO.().linexp(-1, 1, 0.5, 2)) * 6.dbamp; + snd = FreqShift.ar(snd, randomLFO.() * 100); + }; + snd = Limiter.ar(snd); + snd = snd * -2.dbamp; + snd; +}.play(fadeTime: 0); +) + + +