Eurorack DIY 022 – RP2040 Zero Karplus Strong Physical Modelling

Introduction

Features

Schematic

Code

#include <PWMAudio.h>

// sample rate for audio synthesis
#define SAMPLE_RATE 44100
// four voice polyphony
#define N_VOICES 4

// V/Oct input calibration
#define CAL_OFFSET 0.11
#define CAL_FACTOR 3.5

// create the pwm audio device on GPIO 1.
PWMAudio pwm(1);

// create audio buffers
int N[N_VOICES];
int16_t buf[N_VOICES][SAMPLE_RATE/20];
// audio buffer pointer
int bh[N_VOICES];
// frequency depended decay factor for each voice
float rho[N_VOICES];

int v = 0; // current voice

float D = 0.999; // decay parameter
float S = 0.5; // brightness parameter
float beta = 0.5; // pick position parameter
float p = 0;

bool trigger_state = 0;

void cb() {
  while (pwm.availableForWrite()) {
    int16_t value = 0;
    // iterate over each voice
    for (int v=0; v < N_VOICES; v++) {
      // add current value to output "mixer"
      value += buf[v][bh[v]];

      // index calculation
      int bh_0 = bh[v];
      int bh_1 = bh_0 < N[v]-1 ? bh_0+1 : 0;
      
      // calculate extended karplus strong filter
      int16_t z_0  = buf[v][bh_0];
      int16_t z_1  = buf[v][bh_1];       
      int16_t avg = D * rho[v] * ((1-S) * z_0 + S * z_1);

      // apply filter value
      buf[v][bh_0] = avg;

      // shift delay line
      bh[v] = bh_1;      
    }    
    // write value to audio pwm pin
    pwm.write(value);
  }
}

void setup() {      
  // initialize buffer with zeros 
  for (int v=0; v < N_VOICES; v++) {
    for (int i=0; i < SAMPLE_RATE/20; i++)
      buf[v][i] = 0;
    N[v] = 0;
    bh[v] = 0;
    rho[v] = 0;
  }

  // setup pwm audio output
  pwm.onTransmit(cb);
  pwm.begin(SAMPLE_RATE);

  // setup serial port for debugging
  Serial.begin(115200);
  Serial.println("Hello Karplus Strong!");

  // set analog read resolution to 12-bit (0...4095)
  analogReadResolution(12);

  // initialize trigger input
  pinMode(2, INPUT);
  // initialize pick direction switch input with pullup
  pinMode(3, INPUT_PULLUP);
}

void excite(const float freq=440, const int amplitude=32767, const int v=0) {  
  // calculate buffer length from frequency (2x string length)
  N[v] = SAMPLE_RATE / freq;  
  // calculate frequency dependent decay factor (setting rho to achieve a decay of -60 dB in t_60 seconds)
  float t_60 = 128.0;
  rho[v] = pow(0.001, 1.0 / (freq * t_60));
  // initialize buffer with new random values
  for (int i=0; i < N[v]; i++)
    buf[v][i] = random(-amplitude, amplitude);

  // pick direction iir filter
  uint16_t y_1 = 0;
  for (int i = 0; i < N[v]; i++){
    buf[v][i] = ((1-p) * buf[v][i] + p * y_1);
    y_1 = buf[v][i];
  }

  // pick position comb filter
  int pickpos = max(floor(N[v]*beta), 1);
  for (int i = N[v]-1; i > -1; i--)
    buf[v][i] = buf[v][i] - (i-pickpos > -1 ? buf[v][i-pickpos] : 0);  
}

void loop() {  
  // update decay parameter  
  D = 0.5 + 0.5 * (1 - 1 / (pow(10, 4 * analogRead(A1) / 4096.0)));
  // update brightness parameter  
  S = 0.5 * analogRead(A2) / 4096.0;
  // update pick position parameter
  beta = analogRead(A0) / 4096.0;
  // update pick direction parameter
  p = digitalRead(3) ? 0 : 0.9;

  // check if trigger receives a rising edge (inverted through input transistor)
  bool new_trigger_state = !digitalRead(2);
  if (new_trigger_state && !trigger_state) {
    // wait for a short time for the v/oct input to update
    delay(10);

    // take 4 adc measurements to reduce noise
    int16_t adc_val = (analogRead(A3) + analogRead(A3) + analogRead(A3) + analogRead(A3)) / 4;
    // calculate a v/oct value with respect to calibration parameters and input voltage divider
    float v_oct = 1.666666 * CAL_FACTOR / 4096.0 * adc_val - CAL_OFFSET;
    // convert v/oct to midi pitch
    int pitch = round(v_oct * 12 + 36);
    // convert midi pitch to floating point freq
    float freq = pow(2, (pitch - 69.0) / 12.0) * 440;
    // print out values (usefull for calipration)
    Serial.println(v_oct);
    Serial.println(pitch);

    // excite new string vibration
    excite(freq, 8000, v);  

    // switch to next polyphonic voice
    if (++v >= N_VOICES) v = 0;

    // wait a short time to debounce thr trigger input
    delay(20);
  }  
  trigger_state = new_trigger_state;
}

References

[1] https://ccrma.stanford.edu/~jos/pasp/Extended_Karplus_Strong_Algorithm.html

[1] https://www.jstor.org/stable/3680062

[2] https://www.jstor.org/stable/3680063

[2] https://github.com/marcociccone/EKS-string-generator/blob/master/Extended%20Karplus%20Strong%20Algorithm.ipynb

[3] https://www.youtube.com/watch?v=R3LaFRIE0KI