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