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 Wavetable
s
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')