1. SoundBuffers and sampling
TUTORIAL 001 - SoundBuffers & basic operations
This tutorial introduces one of the core elements in
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
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
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.
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.
SoundBuffer(samplerate=44100, channels=2, frames=882000, dur=20.00)
Save a copy -- this time as a WAV file
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.
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()
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
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')