1. SoundBuffers and sampling

TUTORIAL 001 - SoundBuffers & basic operations

This tutorial introduces one of the core elements in pippi: the SoundBuffer class.

The dsp module in pippi is basically a shortcut that provides many easy initialization helpers -- in this case we'll use it to read a WAV file from disk into memory as a SoundBuffer for further manipulation.

from pippi import dsp

Reading and writing sounds

Assuming you run this script from the root of the pippi source directory, here we're loading a 10 second long stereo WAV file recording of an electric guitar which lives in the tests/sounds directory.

guitar = dsp.read('tests/sounds/guitar10s.wav')

Print the type, length in frames, and duration in seconds

print('%s: %s frames / %.2f seconds' % (type(guitar), len(guitar), guitar.dur))
<class 'pippi.soundbuffer.SoundBuffer'>: 441000 frames / 10.00 seconds

Audio file I/O is done with the libsndfile library which supports OGG, FLAC and other compressed soundfile types as well as standard uncompressed PCM audio types. Lets save a copy of this guitar sound as a FLAC in the current directory by calling the write method available to any SoundBuffer.

guitar.write('docs/tutorials/renders/001-guitar-unaltered.flac')

Processing sounds

Many sound transformations are available directly as methods on the SoundBuffer. Lets slow the guitar down to half-speed, print info about it again and then save the result as a new file.

slow_guitar = guitar.speed(0.5)

The string representation of a SoundBuffer has some more interesting info, so we can just print the sound to see the changed information instead.

print(slow_guitar)
SoundBuffer(samplerate=44100, channels=2, frames=882000, dur=20.00)

Save a copy -- this time as a WAV file

slow_guitar.write('docs/tutorials/renders/001-guitar-slow.wav')

Pippi has many other built-in routines for sound processing which will be explored in future tutorials. Check out the methods available on SoundBuffer and the fx module for more examples.

Mixing sounds

We can mix the sounds together and save the result using the mix (&) operator

mixed_guitars = slow_guitar & guitar
mixed_guitars.write('docs/tutorials/renders/001-guitar-mixed.wav')
print(mixed_guitars)
SoundBuffer(samplerate=44100, channels=2, frames=882000, dur=20.00)

Notice how the output buffer is the length of the longest of the two files so nothing is clipped. This is true also when you mix many files at once with dsp.mix([sound1, sound2, sound3]).

Often it's useful to mix many processed segments into a final output buffer. Lets use the dsp.buffer shortcut to create a new empty SoundBuffer. The size of the internal buffer will expand as needed when we dub new sounds into it.

Tip: if you're dubbing hundreds or thousands of sounds and expanding the internal buffer every time, it is useful to give your buffer an initial length. This way the dubs can be done in-place on the buffer without needing to expand it every time. That can be expensive if it is done often!

out = dsp.buffer()

The tune module

The tune module has many helper functions for working with musical pitches and tuning systems.

from pippi import tune

Lets make a list of frequencies that represent a major triad in the key of C, starting at C3 and using a just tuning.

freqs = tune.chord('I', key='C', octave=3, ratios=tune.JUST)

These are the frequencies

print('Cmaj: %s' % freqs)
Cmaj: [132.0, 165.0, 198.0]

In order to use our speed method to pitch shift the guitar and make a C major chord, we need to convert these absolute frequences into relative speeds. The original guitar note is an A at roughly 220hz, so the speeds can be derived by using this value.

A220 is A2 in scientific pitch notation -- we can also use a helper to get the frequency from the note name:

original_freq = tune.ntf('A2')

Now we can make a new list of relative speeds by dividing the original frequency into the target frequency

speeds = [ new_freq / original_freq for new_freq in freqs ]

Lets dub a copy of the guitar note at each of these speeds into our output buffer every 1.5 seconds.

pos = 0  
beat = 1.5
out = dsp.buffer()
for speed in speeds:
    # Create a pitch-shifted copy of the original guitar
    note = guitar.speed(speed)

    # Dub it into the output buffer at the current position in seconds
    out.dub(note, pos)

    # Just for fun, lets also dub another copy 400 milliseconds (0.4 seconds) later that's an octave higher
    note = guitar.speed(speed * 2)
    out.dub(note, pos + 0.4) 

    # Now move the write position forward 1.5 seconds
    pos += beat

# Save this output buffer
out.write('docs/tutorials/renders/001-guitar-chord.wav')

Envelopes, amplitude modulation and Wavetables

This time, lets add a basic amplitude envelope to each note, as well as an overlay of tremelo.

We'll also do something a little more interesting with the harmony

pos = 0  
beat = 1.5
out = dsp.buffer()

# Get the frequencies of an F minor 11th chord tuned to a 
# set of ratios devised by Terry Riley starting on F2
freqs = tune.chord('ii11', key='e', octave=2, ratios=tune.TERRY)

# Convert the frequencies to speeds
speeds = [ freq / original_freq for freq in freqs ]

# Loop 4x through the speeds
for speed in speeds * 4:
    # First lets cut the length of this note shorter to make 
    # it easier to hear how the envelope sounds.
    #
    # `cut(0...` will cut a section from the beginning of the 
    # sound until somewhere between 0.1 and 3 seconds later.
    note = guitar.speed(speed).cut(0, dsp.rand(0.1,3))

    # Apply an ADSR envelope to the note
    note = note.adsr(
        a=dsp.rand(0.05, 0.2), # Attack between 50ms and 200ms
        d=dsp.rand(0.1, 0.3),  # Decay between 100ms and 300ms
        s=dsp.rand(0.1, 0.5),  # Sustain between 10% and 50%
        r=dsp.rand(1, 2)       # Release between 1 and 2 seconds*
    )

    # * If the note is shorter than the sum of the ASDR lengths, 
    #   the release period is adjusted to whatever is left over.

    # This time, lets use the `&` operator to mix each speed, 
    # and pad the beginning of the higher sound with (less) silence 
    # instead of adding an offset to the position when we dub it.
    note = note & note.speed(2).pad(dsp.rand(0, 0.05))

    # Calculate the number of tremelos we want per note based 
    # on the desired tremelo length
    tremelo_length = dsp.rand(0.05, 0.1) # between 50 and 100 milliseconds
    num_tremelos = note.dur // tremelo_length

    # Create a new `Wavetable` with a sinewave repeated `num_tremelos` times.
    # We're creating a window here, which means by default the wavetable will 
    # always go from 0 to 1, and the built-in shapes are period-adjusted so you 
    # just get a single sine hump for example, not both sides of a full sinewave 
    # from -1 to 1 that you'd get using `dsp.wt('sine')` instead.
    tremelo = dsp.win('sine').repeated(num_tremelos) 

    # Multiply the note by the tremelo. This will have the effect of doing an 
    # amplitude modulation on the sound. The object on the right side of the 
    # statement will be resized and interpolated to match the size of the object 
    # on the left side of the statement. 
    # `sound.env('sine')` is the same as `sound * dsp.win('sine')`
    note = note * tremelo

    out.dub(note, pos)
    pos += beat

# Introducing... the `fx` module! 
from pippi import fx

# Lets also normalize the final result to 0db (or a magnitude of 1)
out = fx.norm(out, 1)

# Save this output buffer
out.write('docs/tutorials/renders/001-guitar-chord-enveloped.wav')