19 Jan 2023

Python Playground Chapter 4: Strings

In October 2022, I started reading Python Playground and wrote an article to mark the day I started.

As of today, I finished reading and coding for the fourth chapter of Python Playground. I had a lot of fun with this chapter because it involved simulating a vibrating string using the Karplus-Strong Algorithm.

A vibrating string, when plucked, produces a wave of a certain frequency that oscillates with smaller and smaller amplitudes with time. The amplitude at every oscilliation is given by the Karplus-Strong Algorithm.

The Algorithm

The Karplus-Strong Algorithm loops a short waveform through a short delay to simulate the sound of a plucked string. First a short waveform of N (in this case 44100 samples which is used as the standard frequncy for audio CDs) samples is generated. The waveform is in the form of a ring-buffer of random values in the range [-0.5, 0.5]. A samples buffer is created to store the intensity of the sound at any time. The algorithm to create the plucked string sound is as follows:

  1. The first value from the ring is stored in the samples buffer.

  2. The average of the first two elements of the ring is calculated.

  3. The average value is multiplied by the attenuation factor (0.995).

  4. This value is added to the end of the ring buffer.

  5. The first element is removed from the ring buffer.

The averaging step cuts off higher frequencies which removes the higher values of the fundamental frequency. As a result, the sound that the buffers produce has only the fundamental frequency. This buffer is then converted to a byte-stream and saved as a .wav file.

def generate_note(freq):
    sRate = 44100
    nSamples = 44100
    N = int(sRate/freq)

    buf = deque([random.random() - 0.5 for i in range(N)])

    if gShowPlot:
        axline, = plt.plot(buf)

    samples = np.array([0]*nSamples, 'float32')
    for i in range(nSamples):
        samples[i] = buf[0]
        avg = 0.996 * 0.5 * (buf[0] + buf[1])
        buf.append(avg)
        buf.popleft()
        if gShowPlot:
            if i % 1000 == 0:
                axline.set_ydata(buf)
                plt.draw()

    samples = np.array(samples*32767, 'int16')
    return samples.tobytes()

Fun

After writing the code, I added a feature which would allow the program to read notes from a text file and play them based on a given set of notes and corresponding frequencies.

pmNotes = {'E3':164, 'F3':174, 'G3':196, 'A3':220, 'B3':246, 'C4': 262, 'D4':293, 'Eb4': 311, 'F4': 349, 'G4':391, 'Bb4':466}

# G-major Scale
pmNotesG = {'G4':392, 'A4':440, 'B4':493, 'C5':523, 'D5':587, 'E5':659, 'F5#':739, 'G5':783}

# Minor Pentatonic Scale
peNotes = {'C4': 262, 'Eb': 311, 'F': 349, 'G':391, 'Bb':466}
gShowPlot = False

chords = {
    'G':['G4', 'B4', 'D5'],
    'A':['A4', 'C5', 'E5'],
    'C':['C5', 'E5', 'G5']
}

I created a text-file that contained the main melody of “What do you know?” by Ben Salisbury, which is the opening music to “Annihilation” and recorded the result.

(Read the Annihilation books. They’re pretty good)

That’s all I have for now. Live long and prosper.

References

Python Playground

Electronut Labs

My Code For This Chapter