String sounds with Karplus-Strong Algorithm

Hi Folks,

As part of my sound experiments with Arduino, I ran into the Karplus-Strong algorithm to generate plucked string sounds. Although I haven't implemented this yet in the Arduino (there are limitations on memory and speed which I am not sure how to overcome), I did write some simple code in python that generates sounds using the above. I think it can be used to generate sound samples for the Arduino, if nothing else.

Best Regards

hey!

the Karplus-Strong algorithm is really surprising...

i converted it to C/C++ (ring buffer, random, no wave - just raw audio):

#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <time.h>

int main(int argc, char **argv) {
   srandom(time(0));

   const int sr = argc>3 ? atoi(argv[3]) : 44100;
   const int f = argc>1 ? atoi(argv[1]) : 220;
   const int N = sr / f;
   const int c = 1;
   const int T = argc>2 ? atoi(argv[2]) : 10;
   float buf[N];
   for (int i=N-1; i>=0; i--)
      buf[i] = random() * (1/(float)RAND_MAX) - .5;
   int bh = 0;

   for (int i=T*sr; i>0; i--) {
      const int16_t v = buf[bh] * (1<<(8*sizeof v));
      write(1,&v,sizeof v);
      const int nbh = bh != N-1 ? bh+1 : 0;
      const float avg = .999 * .5 * (buf[bh] + buf[nbh]);
      buf[bh] = avg;
      bh = nbh;
   }
   return 0;
}

i will convert it to arduino C soon... :slight_smile:
no floating point operation...

bye

I would think you will run out of RAM on Arduino using a typical audio sampling rate. anyway, here is that code modifed to compile in Arduino.

void karplus_Strong(unsigned int sr, int f, int T) {

   int N = sr / f;
   const int c = 1;
   float buf[N];
   for (int i=N-1; i>=0; i--)
      buf[i] = random() * (1/(float)RAND_MAX) - .5;
   int bh = 0;

   for (int i=T*sr; i>0; i--) {
      int v = buf[bh] * (1<<(8*sizeof v));
//      write(1,&v,sizeof v);
      int nbh = bh != N-1 ? bh+1 : 0;
      float avg = .999 * .5 * (buf[bh] + buf[nbh]);
      buf[bh] = avg;
      bh = nbh;
   }
}

hey!

maybe it would be better to send the raw audio data via USB and the arduino just outputs the data in the right moment?

here it is and it should fit easily into memory (about 512 bytes on the stack)...

int sendKarplusStrongSound(const uint16_t f /*Hz*/, const uint8_t T /*sec*/) {
   const uint32_t sr = 11025; // sample rate
   const uint8_t N = sr / f; // f == 44Hz..65535Hz

   int16_t buf[N];
   for (uint8_t i=0; i!=N; i++)
      buf[i] = (int16_t) random(-32768,32767);
   uint8_t bh = 0;

   for (uint32_t i=sr*T; i>0; i--) {
      const int8_t v = (int8_t) (buf[bh] >> 8);
      sendTo8bitDAC(v);
      delayMicroseconds(1000000/sr); // or do something else for some usecs
      const uint8_t nbh = bh!=N-1 ? bh+1 : 0;
      register int32_t avg = buf[bh] + (int32_t)buf[nbh];
      avg = (avg << 10) - avg; // subtract avg more than once to get faster volume decrease
      buf[bh] = avg >> 11; // no division, just shift
      bh = nbh;
   }
}

bye

hey!

if the arduino should produce the samples,
and
if the frequency should be higher,
the buffer gets too small,
so that we should maybe use a fixed size buffer and
vary the sample rate...
i found this idea, when i watched this.

int sendKarplusStrongSound(const uint16_t f /*Hz*/, const uint8_t T /*sec*/) {
   const uint8_t N = 32;
   const uint32_t sr = f * N; // sample rate: 800(25Hz)..47619(1488Hz)

   int16_t buf[N];
   for (uint8_t i=0; i!=N; i++)
      buf[i] = (int16_t) random(-32768,32767);
   uint8_t bh = 0;

   const int Tloop = 20; // or is is more?
   const int dt = 1000000/sr - Tloop;
   for (uint32_t i=sr*T; i>0; i--) {
      const int8_t v = (int8_t) (buf[bh] >> 8);
      sendTo8bitDAC(v); // preferably by port operation, e. g. "PORTD = v;"
      delayMicroseconds(dt); // or do something else for <dt> usecs
      const uint8_t nbh = bh!=N-1 ? bh+1 : 0;
      register int32_t avg = buf[bh] + (int32_t)buf[nbh];
      avg = (avg << 10) - avg; // subtract avg more than once to get faster volume decrease
      buf[bh] = avg >> 11; // no division, just shift
      bh = nbh;
   }
}

bye

Riddick, for some reason your previous posted function works better for me than your last one.

I also added some tremolo by reading 3 digital pins (8,9,10) connected to a 4040 IC driven by a clock signal from a 40106 IC.
and changing the line
const int8_t v = (int8_t) (buf[bh] >> 8);
to:
const int8_t v = ((int8_t) (buf[bh] >> 8)) >> (PINB & B00000111);

See http://little-scale.blogspot.com/2008/01/arduino-noise-maker-info.html for more info.

It would be nice if it were possible to sound 3 plucked strings together.
Maybe make it interrupt driven?

Can someone tell me what I am doing wrong?
I am trying to play 3 plucked strings more or less at the same time, but as soon as I uncomment the lines to play s1 or s2 the sound dies.

Any help is appreciated.

/**
 * Trying to play 3 plucked strings at the same time
 *
 *
 *
 **/

#include "avr/pgmspace.h"

// table of 2 octaves 3 & 4 
// Octave 3: C=46, C#=48, D=52, D#=54, E=57, F=61, F#=65, G=68, G#=72, A=76, A#=80, B=86
// Octave 4: C=91, C#=97, D=103, D#=109, E=114, F=120, F#=127, G=134, G#=147, A=155, A#=162, B=170
PROGMEM  prog_uchar notes[]  = {
 46, 48,  52,  54,  57,  61,  65,  68,  72,  76,  80,  86,
 91, 97, 103, 109, 114, 120, 127, 134, 147, 154, 161, 170,
 182
};
// usage: v = pgm_read_byte_near(notes + nc);

#define SAMPLING_RATE  11025
//#define DELAY_TIME     100000/SAMPLING_RATE
#define DELAY_TIME     20000/SAMPLING_RATE

uint16_t v;
uint16_t vc;
uint8_t  nbh;
int32_t  avg;
long     cnt;

// String 1
byte s1;
uint8_t  N1;
int16_t  buf1[250];
uint8_t  bh1;
uint16_t f1;
uint16_t T1;
uint32_t iT1;
uint32_t iTMax1;

// String 2
byte s2;
uint8_t  N2;
int16_t  buf2[250];
uint8_t  bh2;
uint16_t f2;
uint16_t T2;
uint32_t iT2;
uint32_t iTMax2;

// String 3
byte s3;
uint8_t  N3;
int16_t  buf3[250];
uint8_t  bh3;
uint16_t f3;
uint16_t T3;
uint32_t iT3;
uint32_t iTMax3;


// -------------------------------------------------------------------------------------------------------------
// setup()
// -------------------------------------------------------------------------------------------------------------
void setup(){
 pinMode(2, OUTPUT);
 pinMode(3, OUTPUT);
 pinMode(4, OUTPUT);
 pinMode(5, OUTPUT);
 pinMode(6, OUTPUT);
 pinMode(7, OUTPUT);

 pinMode(8, INPUT);
 pinMode(9, INPUT);
 pinMode(10, INPUT);

 pinMode(13, OUTPUT);

 PORTD = 0;

 s1  = 0; // 0 = active
 s2  = 0; // 0 = active
 s3  = 0; // 0 = active
 cnt = 0;
}

// -------------------------------------------------------------------------------------------------------------
// loop()
// -------------------------------------------------------------------------------------------------------------
void loop(){
 
 cnt++;
 if (cnt == 10000)
   cnt=0;

 
 // pluck string at pos 100
 if (s1 == 0 && cnt > 100) {
   f1 = 46;  // freq
   T1 = 200; // duration

   // set buffer size
   N1 = SAMPLING_RATE / f1; // f == 44Hz..65535Hz
   // fill the buffer with white noise
   for (uint8_t i=0; i!=N1; i++) {
     buf1[i] = (int16_t) random(-32768,32767);
   }
   bh1 = 0;
   iTMax1 = SAMPLING_RATE * T1;
   iT1 = 0;
   s1 = 1;
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(5);
 }
 

 // pluck string at pos 4000
 if (s2 == 0 && cnt > 4000) {
   f2 = 57;  // freq
   T2 = 200; // duration

   // set buffer size
   N2 = SAMPLING_RATE / f2; // f == 44Hz..65535Hz
   // fill the buffer with white noise
   for (uint8_t i=0; i!=N2; i++) {
     buf2[i] = (int16_t) random(-32768,32767);
   }
   bh2 = 0;
   iTMax2 = SAMPLING_RATE * T2;
   iT2 = 0;
   s2 = 1;
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(5);
 }

 // pluck string at pos 8000
 if (s3 == 0 && cnt > 8000) {
   //Serial.println("Sound started.");
   f3 = 68;  // freq
   T3 = 200; // duration

   // set buffer size
   N3 = SAMPLING_RATE / f3; // f == 44Hz..65535Hz
   // fill the buffer with white noise
   for (uint8_t i=0; i!=N3; i++) {
     buf3[i] = (int16_t) random(-32768,32767);
   }
   bh3 = 0;
   iTMax3 = SAMPLING_RATE * T3;
   iT3 = 0;
   s3 = 1;
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(5);
 }

 
 v = 0;
 vc = 0;

 /*
 if (s1 == 1) {
   v += (int8_t) (buf1[bh1] >> 8);
   vc++;

   nbh = bh1!=N1-1 ? bh1+1 : 0;
   avg = buf1[bh1] + (int32_t)buf1[nbh];
   avg = (avg << 10) - avg;    // subtract avg more than once to get faster volume decrease
   buf1[bh1] = avg >> 11;      // no division, just shift
   bh1 = nbh;

   iT1++;
   if (iT1 == iTMax1) {
     s1 = 0;
   }
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(10);
 }
 */
 
 /*
 if (s2 == 1) {
   v += (int8_t) (buf2[bh2] >> 8);
   vc++;

   nbh = bh2!=N2-1 ? bh2+1 : 0;
   avg = buf2[bh2] + (int32_t)buf2[nbh];
   avg = (avg << 10) - avg;    // subtract avg more than once to get faster volume decrease
   buf2[bh2] = avg >> 11;      // no division, just shift
   bh2 = nbh;

   iT2++;
   if (iT2 == iTMax2) {
     s2 = 0;
   }
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(10);
 }
 */ 
 
 if (s3 == 1) {
   v += (int8_t) (buf3[bh3] >> 8);
   vc++;

   nbh = bh3!=N3-1 ? bh3+1 : 0;
   avg = buf3[bh3] + (int32_t)buf3[nbh];
   avg = (avg << 10) - avg;  // subtract avg more than once to get faster volume decrease
   buf3[bh3] = avg >> 11;    // no division, just shift
   bh3 = nbh;

   iT3++;
   if (iT3 == iTMax3) {
     s3 = 0;
   }
 }
 else {
   // do something that takes the same time
   //delayMicroseconds(10);
 }
 
 
 v = v / vc;  // >> (PINB & B00000111); // tremolo effect
 sendTo8bitDAC(v); // preferably by port operation, e. g. "PORTD = v;"

 delayMicroseconds(DELAY_TIME);
}

void sendTo8bitDAC(int8_t v) {
 PORTD = v >> 2; // we use a 6 bit R2R ladder
}