The advantage over FOF is that you don’t need grain tracking. AFAICS the advantage isn’t necessarily that it sounds better, though.
Part of the trick is to relate bandwidth from formant tables over to attack and decay times. I’m assuming the right relationship is inverse (as bandwidth goes up, ring time hence attack and decay times should go down) but tuning the sound was (for me, with my poor maths) trial and error and I’m not entirely satisfied with the result. The baseline atk = 0.001 and dcy = 0.008 seems to strike a good balance between letting the pulses through (so that the fundamental is audible) and letting the formants color the sound. IMO FM formants sound better though (but this approach might work well with more tuning).
This code example is based on a FM-formant demo I’d thrown together a couple of years ago; I just deleted the FM bits and replaced them with Formlet usage. I found that both versions are a bit piercing in the 2000-4000 Hz range, suggesting that FormantTable’s amps for the high partials may be too high.
(
s.waitForBoot {
var cond = CondVar.new;
var k = "IEAOU".collectAs({ |chr| ("tenor" ++ chr).asSymbol }, Array);
var formantBuf;
var formantBus = Bus.control(s, 15);
var freqBus = Bus.control(s, 1);
var window, canvas, freqNumber;
// [[ 5freqs, 5bws, 5amps ], [ 5freqs, 5bws, 5amps ]]
var formants = k.collect { |id| FormantTable.at(id) };
// [[ 5freqs, 5freqs, 5freqs ... ], [ 5bws, 5bws, 5bws ... ] ...]
formants = formants.flop;
// [[ low formants for 5 vowels, 2nd formants for 5 vowels etc... ]]
formants = formants.collect(_.flop);
formants.flat.size.debug;
formantBuf = Buffer.sendCollection(s, formants.flat, 1, action: {
cond.signalOne
});
b = formantBuf; c = formantBus; d = freqBus;
~vowelSelector = SynthDef(\vowelSelector, { |out, bufnum, freqOut, x, y|
var vowel = Lag.kr(y, 0.1) * 3.999; // MouseY.kr(0, 3.999);
var offset5 = Array.series(5, 0, 5);
// using BufRd for automatic linear interpolation
var freqs = BufRd.kr(1, bufnum, vowel + offset5, loop: 0);
var bws = BufRd.kr(1, bufnum, vowel + (offset5 + 25), loop: 0);
var amps = BufRd.kr(1, bufnum, vowel + (offset5 + 50), loop: 0);
Out.kr(out, freqs ++ bws ++ amps);
Out.kr(freqOut, Lag.kr(x, 0.1).linexp(0, 1, 50, 800) /*MouseX.kr(50, 800, 1)*/);
}).play(args: [out: formantBus, freqOut: freqBus, bufnum: formantBuf]);
~formants = SynthDef(\formants, { |out, freq = 440, formantBus, atk = 0.001, dcy = 0.008, amp = 0.2|
var freqs, bws, amps;
var quotient, xfades, evenCar, oddCar, sig;
// IMO it sounds better with Blip
// var pulses = Impulse.ar(freq);
var pulses = Blip.ar(freq, 11000 / freq);
#freqs, bws, amps = In.kr(formantBus, 15).clump(5);
// as bandwidth increases, atk and dcy should decrease
sig = Formlet.ar(pulses, freqs, atk / bws, dcy / bws) * amps;
sig = sig.sum;
// empirically, 2000-4000 Hz formants are too prominent
sig = MidEQ.ar(sig, 3000, 1.9, -24);
Out.ar(out, LeakDC.ar(sig).dup);
}).play(args: [formantBus: formantBus, freq: freqBus.asMap]);
~fx = {
var sig = In.ar(0, 2);
sig = sig + DelayC.ar(sig, 0.2,
Array.fill(2, {
var predelay = Rand(0.005, 0.01);
var width = Rand(0.7, 0.98);
SinOsc.kr(ExpRand(0.1, 0.4), Rand(0.5, 3.0))
* (width * predelay) + predelay
})
);
ReplaceOut.ar(0, FreeVerb2.ar(sig[0], sig[1], 0.4, 0.8, 0.3))
}.play(target: s.defaultGroup, addAction: \addToTail);
window = Window("Formants", Rect(800, 200, 500, 500)).front;
window.layout = VLayout(
HLayout(
canvas = Slider2D(),
View().fixedWidth_(50)
.layout_(
VLayout(
StaticText().string_("U"),
nil,
StaticText().string_("O"),
nil,
StaticText().string_("A"),
nil,
StaticText().string_("E"),
nil,
StaticText().string_("I")
)
)
),
HLayout(
nil,
StaticText().string_("freq").fixedWidth_(60),
freqNumber = StaticText().fixedWidth_(60),
nil
)
);
canvas.action = { |view|
~vowelSelector.set(\x, view.x, \y, view.y);
freqNumber.string = view.x.linexp(0, 1, 50, 800).asString;
};
window.onClose = {
[~vowelSelector, ~formants, ~fx].do(_.free);
};
~formants.onFree({
[formantBus, freqBus, formantBuf].do(_.free);
});
};
)
BTW in your Pd forum thread, you mentioned implementing Formlet for Pd – but I think you already had done this before. [resonant2~] help says “[resonant2~] is a resonator just like [resonant~], but you you can specify an attack time besides a decay time” and… that’s Formlet, isn’t it? So no need to have implemented it a second time.
hjh