Cross-synthesis

— reference implementations in different programming environments

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.

Example recording of cross synthesis made with SuperCollider (see below).

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

VCV Rack implementation.

VCV Rack implementation.

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

GUI of the SuperCollider implementation.

GUI of the SuperCollider implementation.

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 ControlSpecs 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

Faust implementation in the web IDE.

Faust implementation in the web IDE.

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

Pure Data implementation.

Pure Data implementation.

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 of phasor~ and cos~ 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 using delwrite~/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.

Max/MSP implementation.

Max/MSP implementation.

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.

Gen implementation in Max/MSP.

Gen implementation in Max/MSP.

The source code for both implementations can be found in this gist.