Cross-synthesis
2024 by Till Bovermann
Cross synthesis is a technique to combine two signals by using the phase of one signal to modulate the amplitude of the other signal.
I wrote some reference implementations of this technique in different audio programming environments to compare the different approaches and to provide a starting point for further exploration.
The straightforward implementation of cross synthesis consists of two oscillators, where the output of each oscillator is fed back into the phase of the other oscillator. More interesting results can be achieved by adding a delay line into the modulation path.
So far, I have implemented the algorithm in SuperCollider, Faust, Pure Data, Max/MSP, and VCV Rack (Cardinal). The source code for most of the implementations can be found in this gist.
VCV Rack Link to heading
This implementation serves as a kind of reference that may also halp to realise cross-synthesis in a hardware modular rack. In difference to the other implementations, the VCV Rack implementation is based around frequency modulation rather than phase modulation which are, however, closely related.
Since the VCV Rack file format is not binary, I do not provide it in the gist but think that it is anyway relatively easy to replicate.
SuperCollider Link to heading
SuperCollider has a block size of 64 samples by default. To achieve 1-sample feedback, the block size has to be set to 1. This can be done by executing the following code in the SuperCollider IDE:
s.options.blockSize = 1; // 1-sample feedback
s.reboot;
We can limit the possible range of the parameters in the Ndef
GUI by adding ControlSpec
s to the Spec
class:
Spec.add(\freq0, \freq);
Spec.add(\freq1, \freq);
Spec.add(\fb0, [-1, 1, \lin]);
Spec.add(\fb1, [-1, 1, \lin]);
Spec.add(\dt0, [0, 1000, \lin, 1, 1]);
Spec.add(\dt1, [0, 1000, \lin, 1, 1]);
Below is the complete implementation of cross-synthesis in SuperCollider.
Note that you can easily adjust the number of oscillators by changing the numOscs
variable.
Ndef(\cross, {
var numOscs = 2;
var freqs = numOscs.collect{|i|
freq%".format(i).asSymbol.kr(0)
};
// delay in samples
var dts = numOscs.collect{|i|
dt%".format(i).asSymbol.kr(0)
};
var fbs = numOscs.collect{|i|
fb%".format(i).asSymbol.kr(0)
};
// feedback input (rotated by one)
var ins = LocalIn.ar(numOscs).rotate;
var phases = DelayN.ar(ins * fbs, 1, dts / s.sampleRate);
var snds = SinOsc.ar(freqs, phases * pi);
// feedback output
LocalOut.ar(snds);
// splay to stereo
Splay.ar(snds);
})
Ndef(\cross).edit
Faust Link to heading
The faust implementation is by far the shortest and most concise.
Feedback is achieved by using ~
operator, combined with the cross
function that takes the frequency, amplitude, and delay time of two oscillators as arguments.
import("stdfaust.lib");
maxDelay = 100;
f1 = hslider("f1", 100.0, 0, 1000, 0.1) : si.smoo;
f2 = hslider("f2", 100, 0, 1000, 0.1) : si.smoo;
d1 = hslider("d1", 0, 0, maxDelay, 0.01) : si.smoo;
d2 = hslider("d2", 0, 0, maxDelay, 0.01) : si.smoo;
a1 = hslider("a1", 0.1, -1, 1, 0.001) : si.smoo;
a2 = hslider("a2", 0.1, -1, 1, 0.001) : si.smoo;
vol = hslider("vol", 0.1, 0, 1, 0.001) : si.smoo;
process = cross(f1, f2) ~ si.bus(2) with {
cross(f1, f2, p1, p2) =
os.oscp(f1, a1 * phase(p2, d1)),
os.oscp(f2, a2 * phase(p1, d2));
phase(p, dt) = p * ma.PI : de.fdelay(maxDelay, dt);
};
Try it in your browser.
PureData Link to heading
In Pd, there are two challenges to overcome:
- Instead of using the standard
osc~
object, we have to implement the phase lookup with a combination ofphasor~
andcos~
objects. - We have to make sure to not create an infinite feedback loop; this can be achieved by either using send/receive pairs (
s~
/~r
), which act as a 1-block buffer, or by usingdelwrite~/delread~
pairs that allow to adjust the feedback delay in milliseconds.
The source code for this implementation can be found in this gist.
Max/MSP Link to heading
Max/MSP allows for two different implementations: one in the standard Max/MSP environment and one in the Gen environment.
The implementaiton in pure Max/MSP is quite similar to the one in Pure Data, whereas the Gen implementation is more similar to the Faust implementation in that it allows direct feedback paths without the need for send/receive pairs.
The source code for both implementations can be found in this gist.