In the world of synthesizers, sound is made in many different ways. Old, analog synthesizers passed harmonic waves like sine and cosine through simple electrical circuits to generate noise, while modern digital synthesizers can play a recording of practically everything heard by human ears. But one of the most interesting ways of making sound is simulating plucked strings. Interestingly, physical string modeling can be done using a very simple, computationally affordable algorithm.

Introduction

The Karplus-Strong algorithm was invented by Alexander Strong and analyzed by Kevin Karplus in the 1980’s. It goes as follows:
◼
  • Take any wave, like a sine wave
  • ◼
  • Play it once
  • ◼
  • Pass the wave through a low-pass filter - in essence, a filter that removes the extremes from the wave
  • ◼
  • Play the wave again
  • ◼
  • Repeat the above steps until the wave is practically flat
  • While Karplus and Strong realized that their algorithm produced sounds remarkably like a plucked string no matter what initial wave was given, neither was able to prove that what they’d discovered was actually a method of physical string modeling.
    It was only when Julius O. Smith recognized that the low-pass filter in the algorithm was analogous to the loss of energy in a vibrating string that KS synthesis saw renewed interest.
    Today, digital waveguide synthesis is a more advanced, generalized technique for modeling vibrations in instruments - not just in plucked strings, but in all manner of acoustic tubes, drums, bowed instruments, and more. Still, the roots of our modern tools lie in KS synthesis, and the analogue approach is still used in many synthesizers because of its relative simplicity.

    Implementation

    To begin, we figure out how to play waveforms:
    The Wolfram
    Play
    interface works well with any functional curve, so we use that:
    Play[Sin[t*2*Pi],{t,0,1}]
    Out[]=
    We also want to create an initial waveform:
    In[]:=
    excitement[t_]:=RandomReal[{-1,1}]
    In[]:=
    pts=Table[{x,excitement[x]},{x,0,12*Pi,1}];
    ListLinePlot[pts]
    Out[]=
    The following lines are used to normalize the wave - in other words, to make the lowest point of the wave -1 and the highest point 1. This isn’t strictly necessary for the algorithm to work, but it can help produce more even results by scaling every wave to the same amplitudes.
    We extract and x and y coordinates from the points:
    In[]:=
    xs=#[[1]]&/@pts;​​ys=#[[2]]&/@pts;
    And scale the points to normalize:
    In[]:=
    pts={(#[[1]]-Min[xs])/(Max[xs]-Min[xs]),(#[[2]]-Median[ys])}&/@pts;
    We then sort the points by x-axis so that they go from left to right:
    In[]:=
    pts=Sort[pts,#1[[1]]<#2[[1]]&];
    The low-pass filter we use here is one of only many implementations - we mainly use it because it’s incredibly simple.
    The filter takes every point on the wave and averages it with the previous point. That is,
    p
    i
    =
    p
    i
    +
    p
    i-1
    2
    .
    In[]:=
    lowpassFilter[tables_]:=Module[{table=Join[{tables[[1]][[1]]},tables[[1]]]},​​Join[{Table[{table[[i]][[1]],(table[[i-1]][[2]]+table[[i]][[2]])/2},{i,2,Length[table]}]},tables]​​]
    We then generate multiple passes of this filter - that is, we pass the waveform through the filter multiple times, keeping track of the wave after each run.
    In[]:=
    numberOfPasses=100;
    In[]:=
    funcs=Interpolation[#]&/@Nest[lowpassFilter,{pts},numberOfPasses];
    ListAnimate[Plot[#[x],{x,0,1},PlotRange->1,Filling->Bottom]&/@Reverse[funcs]]
    Out[]=
    We then make a function to combine all the passes together end to end. This is essentially the same thing as playing each one right after another.
    In[]:=
    combined[x_]:=Reverse[funcs][[Ceiling[x*numberOfPasses]]][Mod[x,1/numberOfPasses]*numberOfPasses]
    Plot[combined[t],{t,0,1},PlotRange->1]
    Out[]=
    Then we generate the plucked sound:
    plucked=Play[combined[t/0.4],{t,0,1,0.4},SampleRate->100000]
    Out[]=
    That sounds pretty good, so I’d like to play a quick song with it.

    Playing a Song

    Even though KS synthesis is relatively lightweight, I don’t want to generate a new waveform for every note in a song. Instead, I’d prefer to pitch our already generated sound up and down to create each note.
    We do this using Fourier transforms, which are beyond the scope of this essay. The important thing to know is that we can use them to determine the frequencies present in a soundbite, which is exactly what we need to know how much to pitch our recording up and down.
    In[]:=
    data=Table[combined[t],{t,0,1,0.001}];
    We generate a list of frequencies in the soundbite:
    In[]:=
    ft=Fourier[ys,FourierParameters->{-1,-1}];​​sr=100;​​nn=Length[ys];​​freqs=Table[f,{f,0,sr(nn-1)/nn,sr/nn}];
    Now we want to know what our frequences should be to get the notes we want. We start with C (261.63 Hz), and move 1 note (
    1
    12
    2
    Hz) up at a time.
    In[]:=
    frequencies=Table[261.63*(2^(1/12))^t,{t,-36,-25}];
    In[]:=
    notes={"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"};
    We then go through each frequency and pitch up the original sound to get the note we want.
    By taking the mean of the frequencies, we get an approximation of the original frequency that we can use to find the ratio to the target frequency:
    In[]:=
    chromatic=AudioPitchShift[plucked,#/Mean[freqs]]&/@frequencies;
    Then we create a sound library from those notes:
    In[]:=
    soundLib=AssociationThread[notes->chromatic];
    We write the notes and durations out for our song:
    In[]:=
    notes={"C","C","G","G","A","A","G","F","F","E","E","D","D","C"};​​noteDurations={1,1,1,1,1,1,2,1,1,1,1,1,1,2};
    In[]:=
    song=Thread[notes->noteDurations];
    And we get a wonderful song!
    twinkletwinkle=AudioJoin[AudioTimeStretch[soundLib[[#[[1]]]],#[[2]]]&/@song]
    Out[]=
    00:00
    00:06
    Data in CloudObject[
    ...-31-47.wav
    ]
    This final step is a little disconnected, but it serves only to remove some of the digital graininess from our recording. A Wiener filter works in a similar way to a regression function, by minimizing the squared error (or residual).
    If the above audio embed does not work, the same recording is available at: https://www.wolframcloud.com/env/willchoikim/Audio/Audio-2024-09-17T00-31-52.wav.

    Author’s Notes

    Some small considerations after completing this project are in order. To begin, I’m certain there must be a way to reduce the time needed to execute the code written here - it might involve rewriting portions, but as it stands, none of the tools here could run in real time.
    There’s also a potential extension in playing around with parameters to mimic different instruments - I’ve gone for something similar to an amateur violin player in this essay, but in theory, Karplus-Strong synthesis should be able to model most plucked-string sounds.
    I also did try starting with different waveforms, but as Karplus and Strong noted, the initial excitation state seems to barely affect the final sound once normalized and interpolated.
    It’s also important to note that the long notes you hear in the final song are not sustained vibrations. Instead, they’re a trick - it’s the same recording, just lengthened in post. This is not accurate to how the strings would actually sound, but Karplus-Strong synthesis is poorly situated to model sustained vibration.

    Citations

    ◼
  • https://crypto.stanford.edu/~blynn/sound/karplusstrong.html
  • ◼
  • http://sites.music.columbia.edu/cmc/MusicAndComputers/chapter4/04_09.php
  • ◼
  • https://ocw.mit.edu/courses/6-011-introduction-to-communication-control-and-signal-processing-spring-2010/f135b328c7448bf21c4939ea9ff8f8fb_MIT6_011S10_chap11.pdf
  • ◼
  • https://learn.bela.io/tutorials/pure-data/audio/virtual-string-synthesis/#:~:text=Karplus%2DStrong%20synthesis%20was%20first,delay%20line%20back%20into%20itself.
  • ◼
  • https://reference.wolfram.com/language/ref/WienerFilter.html
  • CITE THIS NOTEBOOK

    Physical plucked string modeling using the Karplus-Strong algorithm​
    by William Choi-Kim​
    Wolfram Community, STAFF PICKS, September 17, 2024
    ​https://community.wolfram.com/groups/-/m/t/3275610