About

During this five day workshop (July 22-26, 2019 — CCRMA, Stanford University), participants learned how to program microcontrollers with the Faust programming language for bare-metal high efficiency/low latency real-time audio Digital Signal Processing (DSP). Final projects consisted of hardware for musical applications such as digital guitar pedal effects and synthesizer modules.

The Teensy 3.6 board was used as the main development platform. Its ARM Cortex-M4 microcontroller provides plenty of processing power to implement advanced DSP algorithms (e.g., feedback delay networks, physical models, band-limited oscillators, filter banks, etc.). Also, its various analog and digital inputs can be used for sensors acquisition. The lack of Operating System allows for the use of very low block sizes (i.e., 8 samples) offering extremely low audio latency.

The focus of the workshop was on designing guitar pedal effects or synthesizer modules. To integrate this work into a final enclosure, students had access to the CCRMA prototyping lab ("MaxLab") where they learned how to make bespoke enclosures for their electronics using digital fabrication techniques (e.g., 2D CAD modeling, laser cutting, etc.).

Student projects were featured during a showcase on the CCRMA stage at the end of the workshop.

Covered Topics

Instructors

Lab Kit

The workshop registration fee included a $100 lab kit containing all the required elements (i.e., microcontroller, audio codec, electronic components, casing, etc.) to build your own modular synth module, guitar pedal, etc. Each participant left with his own custom piece of hardware!

Additional Information

This workshop is intended for musicians, makers, engineers, computer scientists, etc. Previous background in computer programming and sound synthesis/processing is preferred but not mandatory. Participants should bring their own laptop. A lab kit fee of $100 will be added to the regular registration fee. Lab kits will include all the necessary elements to complete final projects (e.g., Teensy 3.6, Audio CODEC/Shield, knobs, switches, buttons, plastic for laser cutting, misc electronic components, cabling/wiring, etc.). Feel free to contact the workshop instructors for additional information.


Final Projects


Resources

Code from the Workshop

Faust Delay

import("stdfaust.lib");
delay(d) = de.delay(192000,d);
del = hslider("del(s)",1,0.001,1,0.001)*ma.SR : si.smoo;
gain = hslider("gain",1,0,1,0.01) : si.smoo;
process = delay(del)*gain;

Feed Forward Flanger & Tremolo

import("stdfaust.lib");
flanger(freq,wet,depth) = _ <: de.delay(256,d)*wet,_ :> /(2)
with{
    d = (os.osc(freq) + 1)*127*depth;
};
tremolo(freq,depth) = _*(1-(os.osc(freq)*0.5 + 0.5)*depth);
f = hslider("Frequency",10,0.1,100,0.01);
w = hslider("Wet",0.5,0,1,0.01);
d = hslider("Depth",0.5,0,1,0.01);
process = flanger(f,w,d) : tremolo(5,1) <: _,_;

Phasor

import("stdfaust.lib");
phasor(p) = _~(_+1)%p : _/p*2-1;
p = hslider("period",100,5,1000,0.01);
process = phasor(p);

Echo

import("stdfaust.lib");
echo(del,f) = +~de.delay(50000,d)*f
with{
    d = del*ma.SR;
};
process = echo(0.1,0.5);

Karplus-Strong

import("stdfaust.lib");
ks(freq,damping) = +~(de.fdelay4(1024,d)*damping : filter)
with{
    d = ma.SR/freq-1;
    filter = _ <: _',_ :> /(2);
};
excitation = button("gate") : ba.impulsify;
excitation2 = button("gate") : en.ar(0.001,0.001)*no.noise;
freq = hslider("freq",400,50,2000,0.01);
gain = hslider("gain",1,0,1,0.01);
process = excitation2*gain : ks(freq,0.99) <: _,_;

Switch From a Button on the Arduino

void setup() {
  Serial.begin(9600);
}

bool once = true;
bool change = true;

void loop() {
  int sensorValue = analogRead(A9);
  if(sensorValue > 50){
    if(once){
      if(change){
        Serial.println("on");
        change = false;
      }
      else{
        Serial.println("off");
        change = true;
      }
      once = false;
    }
  }
  else{
    once = true;
  }
  delay(10);
}

Pattern Matching/Iteration

import("stdfaust.lib");
synths(0) = os.osc(440);
synths(1) = os.sawtooth(440);
synths(2) = os.triangle(440);
/*
gains(0) = 0.5;
gains(1) = 0.67;
gains(2) = 0.76;
*/
gains(i) = ba.take(i+1,(0.5,0.67,0.76));
process = par(i,3,synths(i)*gains(i)) :> _;

Filter Bank

import("stdfaust.lib");
filterBank(N) = hgroup("FilterBank",seq(i,N,vgroup("band%i",fi.peak_eq(level(i),freq(i),300))))
with{
    level(j) = vslider("level%j",0,-90,12,0.01);
    freq(j) = vslider("freq%j[style:knob]",500*(j+1),50,5000,0.01);
};
process = filterBank(10);

Noise Gate

import("stdfaust.lib");
noiseGate(threshold,att) = _ <: _*(((abs(_) : si.smoo) < tLinear) : si.smooth(pole))
with{
    tLinear = threshold : ba.db2linear;
    pole = att : ba.tau2pole;
};
process = noiseGate(-30,0.001);

Sine Wave With Smaller Table Size

import("stdfaust.lib");
oscsin(freq) = rdtable(tablesize, os.sinwaveform(tablesize), int(os.phasor(tablesize,freq)))
with{
	tablesize = 1 << 10;
};
process = oscsin(440);

Low-Memory Footprint Zita

import("stdfaust.lib");

zita_rev_fdn(f1,f2,t60dc,t60m,fsmax) =
  ((si.bus(2*N) :> allpass_combs(N) : feedbackmatrix(N)) ~
   (delayfilters(N,freqs,durs) : fbdelaylines(N)))
with {
  N = 8;

  // Delay-line lengths in seconds:
  apdelays = (0.020346, 0.024421, 0.031604, 0.027333, 0.022904,
              0.029291, 0.013458, 0.019123); // feedforward delays in seconds
  tdelays = ( 0.153129, 0.210389, 0.127837, 0.256891, 0.174713,
              0.192303, 0.125000, 0.219991); // total delays in seconds
  tdelay(i) = floor(0.5 + ma.SR*ba.take(i+1,tdelays)); // samples
  apdelay(i) = floor(0.5 + ma.SR*ba.take(i+1,apdelays));
  fbdelay(i) = tdelay(i) - apdelay(i);
  // NOTE: Since SR is not bounded at compile time, we can't use it to
  // allocate delay lines; hence, the fsmax parameter:
  tdelaymaxfs(i) = floor(0.5 + fsmax*ba.take(i+1,tdelays));
  apdelaymaxfs(i) = floor(0.5 + fsmax*ba.take(i+1,apdelays));
  fbdelaymaxfs(i) = tdelaymaxfs(i) - apdelaymaxfs(i);
  nextpow2(x) = ceil(log(x)/log(2.0));
  maxapdelay(i) = int(2.0^max(1.0,nextpow2(apdelaymaxfs(i))));
  maxfbdelay(i) = int(2.0^max(1.0,nextpow2(fbdelaymaxfs(i))));

  apcoeff(i) = select2(i&1,0.6,-0.6);  // allpass comb-filter coefficient
  allpass_combs(N) =
    par(i,N,(fi.allpass_comb(maxapdelay(i),apdelay(i),apcoeff(i)))); // filters.lib
  fbdelaylines(N) = par(i,N,(de.delay(1024,(fbdelay(i)))));
  freqs = (f1,f2); durs = (t60dc,t60m);
  delayfilters(N,freqs,durs) = par(i,N,filter(i,freqs,durs));
  feedbackmatrix(N) = ro.hadamard(N);

  staynormal = 10.0^(-20); // let signals decay well below LSB, but not to zero

  special_lowpass(g,f) = si.smooth(p) with {
    // unity-dc-gain lowpass needs gain g at frequency f => quadratic formula:
    p = mbo2 - sqrt(max(0,mbo2*mbo2 - 1.0)); // other solution is unstable
    mbo2 = (1.0 - gs*c)/(1.0 - gs); // NOTE: must ensure |g|<1 (t60m finite)
    gs = g*g;
    c = cos(2.0*ma.PI*f/float(ma.SR));
  };

  filter(i,freqs,durs) = lowshelf_lowpass(i)/sqrt(float(N))+staynormal
  with {
    lowshelf_lowpass(i) = gM*low_shelf1_l(g0/gM,f(1)):special_lowpass(gM,f(2));
    low_shelf1_l(G0,fx,x) = x + (G0-1)*fi.lowpass(1,fx,x); // filters.lib
    g0 = g(0,i);
    gM = g(1,i);
    f(k) = ba.take(k,freqs);
    dur(j) = ba.take(j+1,durs);
    n60(j) = dur(j)*ma.SR; // decay time in samples
    g(j,i) = exp(-3.0*log(10.0)*tdelay(i)/n60(j));
  };
};

// Stereo input delay used by zita_rev1 in both stereo and ambisonics mode:
zita_in_delay(rdel) = zita_delay_mono(rdel), zita_delay_mono(rdel) with {
  zita_delay_mono(rdel) = de.delay(2700,ma.SR*rdel*0.001) * 0.3;
};

// Stereo input mapping used by zita_rev1 in both stereo and ambisonics mode:
zita_distrib2(N) = _,_ <: fanflip(N) with {
   fanflip(4) = _,_,*(-1),*(-1);
   fanflip(N) = fanflip(N/2),fanflip(N/2);
};

zita_rev1_stereo(rdel,f1,f2,t60dc,t60m,fsmax) =
   zita_in_delay(rdel)
 : zita_distrib2(N)
 : zita_rev_fdn(f1,f2,t60dc,t60m,fsmax)
 : output2(N)
with {
 N = 8;
 output2(N) = outmix(N) : *(t1),*(t1);
 t1 = 0.37; // zita-rev1 linearly ramps from 0 to t1 over one buffer
 outmix(4) = !,ro.butterfly(2),!; // probably the result of some experimenting!
 outmix(N) = outmix(N/2),par(i,N/2,!);
};

zita_light = hgroup("zita",(_,_ <: zita_rev1_stereo(rdel,f1,f2,t60dc,t60m,fsmax),_,_ : 
	out_eq,_,_ : dry_wet : out_level))
with{
	fsmax = 48000.0;  // highest sampling rate that will be used
	rdel = 60;
	f1 = 200;
	t60dc = 3;
	t60m = 2;
	f2 = 6000;
	out_eq = pareq_stereo(eq1f,eq1l,eq1q) : pareq_stereo(eq2f,eq2l,eq2q);
	pareq_stereo(eqf,eql,Q) = fi.peak_eq_rm(eql,eqf,tpbt), fi.peak_eq_rm(eql,eqf,tpbt)
	with {
		tpbt = wcT/sqrt(max(0,g)); // tan(PI*B/SR), B bw in Hz (Q^2 ~ g/4)
		wcT = 2*ma.PI*eqf/ma.SR;  // peak frequency in rad/sample
		g = ba.db2linear(eql); // peak gain
	};
	eq1f = 315;
	eq1l = 0;
	eq1q = 3;
	eq2f = 1500;
	eq2l = 0;
	eq2q = 3;
	dry_wet(x,y) = *(wet) + dry*x, *(wet) + dry*y 
	with {
		wet = 0.5*(drywet+1.0);
		dry = 1.0-wet;
	};
	drywet = vslider("dryWet",
		0,-1.0,1.0,0.01) : si.smoo;
	gain = vslider("level", -6, -70, 40, 0.1) : ba.db2linear : si.smoo;
	out_level = *(gain),*(gain);
};

process = zita_light;

Embedded Systems for Low-Latency Audio DSP


Synth Module Circuitry

The workshop lab kit contains the core elements to make a eurorack module. However, some circuitry needs to be added to the Teensy and its audio shield in order to implement voltage control (VC) or to interface (i.e., impedance matching, etc.) its audio inputs and outputs with other eurorack modules. This short section provides some schematics and useful information on how to do this.

NOTE: some of the figures presented in this section were freely inspired by resources available on the OWL project GitHub. Additional information might be found there as well.

M08X2

Eurorack modules are powered through a M08X2 port which can be found in their back. They carry 12v and 5v power as well as ground. It can be used as the power source for the various elements presented in the following sections as well as for the Teensy (+5V).

Quantity Part Type Value Part ID
1 M08X2 M08X2 M08X2
3 Diode 1N4001 D1, D2, D3
3 Fuse PTCPTH F1, F2, F3

Voltage Control (VC)

1/8" audio jack for voltage control can be connected to any of the analog input of the Teensy using the following circuit where ADC is the analog input on the Teensy and CV_D the audio jack. Power (i.e., -12V/+12V) and ground should be taken directly from the M08X2 port.

Quantity Part Type Value Part ID
1 1/8" Jack Input 1/8" Jack Input CV_D
2 Potentiometer Potentiometer POT4, POT8
1 Resistor 100R R805
1 Resistor 1k R804
1 Resistor 68k R803
1 Resistor 100k R801
2 Resistor 240k R800, R802
1 Op-Amp TL074 IC2D
1 Diode BAT54S D800

Audio Input

Eurorack-compliant audio inputs can be implemented using the following circuit where Audio Jack should be the 1/8" audio jack input and Audio In one of the two audio line input on the Teensy Audio Shield. Power (i.e., -12V) and ground should be taken directly from the M08X2 port.

Quantity Part Type Value Part ID
1 1/8" Jack Input 1/8" Jack Input CV_D
1 Resistor 1k R310
1 Resistor 33k R308
3 Resistor 100k R300, R302, R304
1 Op-Amp TL074 IC2D
1 Diode BAT54S D300

Audio Output

Eurorack-compliant audio outputs can be implemented using the following circuit where Audio Jack should be the 1/8" audio jack output and Audio Out one of the two audio line output on the Teensy Audio Shield. Power (i.e., -12V) and ground should be taken directly from the M08X2 port.

Quantity Part Type Value Part ID
1 1/8" Jack Input 1/8" Jack Input CV_D
1 Resistor 100R R406
1 Resistor 1k R400
1 Resistor 3k3 R404
1 Resistor 6k8 R402
1 Op-Amp TL074 IC2D