Closed vitreo12 closed 1 year ago
Hi!
You can express it by using a branch:
let modulator = (sine_hz(1.0) + 1.0) * 0.5;
let synth = (saw_hz(100.0) | modulator >> (mul(3000.0) ^ mul(0.2))) >> moog();
In a generative spirit, a simple way to accomplish the same is a closure. However, in this case the deterministic pseudorandom phase system will assign different (random) initial phases for the two channels.
// Do not use this unless it is okay that frequency and Q have different phases!
let modulator = || (sine_hz(1.0) + 1.0) * 0.5;
let synth = (saw_hz(100.0) | modulator() * 3000.0 | modulator() * 0.2) >> moog();
Envelopes are not influenced by the pseudorandom phase system. A multichannel envelope is also fairly convenient:
let modulator = lfo(|t| {
let m = (sin_hz(1.0, t) + 1.0) * 0.5;
(m * 3000.0, m * 0.2)
});
let synth = (saw_hz(100.0) | modulator) >> moog();
As the modulator is consumed when used in an expression, it is not so simple to send it to different nodes altogether. I think duplicating the work by using a closure, maybe one that generates an envelope, is an okay way to get around this restriction:
let modulator = || lfo(|t| (sin_hz(1.0, t) + 1.0) * 0.5);
let synth = (saw_hz(100.0) | modulator() * 3000.0 | modulator() * 0.2) >> moog();
Thanks for your answer!
I guess my main problem with the closure approaches is that we would actually be computing the modulator()
sample twice, when only one sine
would be sufficient. So, for this case, I would probably use your first example.
However, another question then pops to mind: would it be possible to "patch" the modulator
to the inputs of two unrelated nodes?
For two unrelated nodes, it's not possible - some mechanism is needed to coordinate the work done by the modulator
in computing samples.
I could probably solve it like this:
fn saw_moog(freq: f64, cutoff: f64, q: f64) -> An<impl AudioNode<Sample = f64, Inputs = U1, Outputs = U1>> {
(saw_hz(freq) | pass() >> (mul(cutoff) ^ mul(q))) >> moog()
}
let modulator = (sine_hz(1.0) + 1.0) * 0.5;
let node = modulator >> (saw_moog(100.0, 3000.0, 0.4 ) ^ saw_moog(200.0, 1000.0, 0.2)) >> join::<U2>();
which I think is actually a very elegant way of expressing a DSP graph
That's a nice way of expressing it! The join
in the last line averages together the channels. If it's okay to sum instead, then it can be simplified a bit by using the bus operator:
let node = modulator >> (saw_moog(100.0, 3000.0, 0.4) & saw_moog(200.0, 1000.0, 0.2));
This is even more elegant, thanks!
I have one more question: is there a way to achieve single sample feedback circuits? I see that there are the feedback
and tick
operators, but how would I feed the output of the feedback back to, for example, the frequency of the oscillator?
sine(440) ---> feedback ---> * 100
^ |
| |
------------------------------
Something like this works (is it doing single sample feedback or block feedback?):
dc(440.0) >> feedback(sine() * 100.0) * 0.01;
Is there a way to get the output of the sine()
instead of the feedback
? This way I wouldn't have to scale the amplitude down.
EDIT:
I should read the docs a little better before posting, here's the solution:
dc(440.0) >> feedback2(sine(), mul(100.0));
Hi there!
First of all: great work on this library! It's a real joy to use and it definitely pushes DSP development in Rust forward.
I'm sorry to open an issue for this, but I couldn't find an answer in the examples. How could I patch one signal to different inputs of a node (and, if possible, of different nodes)?
Of course Rust comes in by not allowing to use a moved value (
modulator
) on the second occurrence. I wonder: how would I express this using a morefundsp
syntax?