SamiPerttu / fundsp

Library for audio processing and synthesis
Apache License 2.0
798 stars 43 forks source link

Patching a signal to multiple inputs #30

Closed vitreo12 closed 1 year ago

vitreo12 commented 1 year ago

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)?

let modulator = (sine_hz(1.0) + 1.0) * 0.5;
let synth = (saw_hz(100.0) | modulator * 3000.0 | modulator * 0.2) >> moog();

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 more fundsp syntax?

SamiPerttu commented 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();
vitreo12 commented 1 year ago

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?

SamiPerttu commented 1 year ago

For two unrelated nodes, it's not possible - some mechanism is needed to coordinate the work done by the modulator in computing samples.

vitreo12 commented 1 year ago

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

SamiPerttu commented 1 year ago

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));
vitreo12 commented 1 year ago

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
  ^                              |
  |                              |
   ------------------------------
vitreo12 commented 1 year ago

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));