An array is an ordered sequence of values; you cannot rearrange values without changing the meaning). A two-dimensional array is just a normal "table".
In python, we will use arrays from the
(numerical python) module. Note that these arrays are
creates an array in memory, storing each value at the appropriate index:
Index 0 1 2 3 Value 3 -2 987 12
Initializing and displaying:
import numpy # initialize all positions with 0 arr = numpy.zeros(8) print arr
Arrays, functions, math, and graphs:
import numpy import pylab import math # this modifies arr directly. # can be dangerous! def modify_array(arr): # len(x) returns the length of x for i in range(len(arr)): arr[i] = math.exp(i) return arr arr1 = numpy.zeros(8) arr1 = modify_array(arr1) arr2 = numpy.zeros(6) # do math on entire array at once arr2 = (arr2 + 1) * 400 pylab.plot(arr1) pylab.plot(arr2) pylab.show()
Display graphs for three sine waves on the same graph.
Pick a sampling rate sufficient to demonstrate whatever you think we're trying to demonstrate.
Most of the audio done in this class will be through wav files, since these are easily created in a cross-platform manner. (this is so you can do homework at home instead of in the lab -- aren't I nice? :)
One warning is that python tries to avoid losing any precision.
If you do an operation like dividing by 2 using integer types, you
would normally lose prevision -- as a result, python automatically
switches to floats. That's generally a good idea, but most audio
programs only support wavfiles with samples in 16-bit
integer format, not 64-bit float. As a result, we
convert the samples back into
int16 before writing
Now, the wav file used in this example is a mono, 16-bit sample one, and the largest and smallest numbers that can be stored in 16 binary digits are respectively 32767 and -32768. It'll make life much easier with essentially no loss of quality if we pretend the range is actually ±32767, i.e. 215-1, when we scale values for writing into the sound file. However, when reading from a sound file, we'll use a scaling factor of ±32768 because there's always a chance that some dangerous geek has actually stored a value of -32768 in it.
Reading and writing a file. Download a440-1second.wav and place it in the
same directory as your
import numpy import scipy.io.wavfile # the double ** is a power operator, i.e. 2^15 convert_16_bit = float(2**15) sample_rate, samples = scipy.io.wavfile.read( "a440-1second.wav") print "Data type is:", samples.dtype # scale to -1.0 -- 1.0 samples = samples / (convert_16_bit + 1.0) print "Data type is now:", samples.dtype # change the file samples = samples * 0.25 # scale to -32768 -- 32767 samples = numpy.int16( samples * convert_16_bit ) print "Data type is now:", samples.dtype scipy.io.wavfile.write("quieter.wav", sample_rate, samples)
Two steps to this exercise:
So far we have been creating the entire audio file in a single array. That's fine on desktop computers creating 1 second of audio (assuming 44100 Hz with 16-bit samples, that's 44100*16/8/1024 = 86 Kb memory), but what if you wanted to create an hour's worth of audio at once? That would be 300 megs of memory! do-able on modern desktops, but kind-of a waste.
So instead, we will create audio in small bits and pieces. A buffer size of 2048 is fairly traditional. How many buffers will we need to create 1 second of audio?
In order to generate a sine wave that is "continuous" over multiple buffers, we need store the state -- the x in sin(x). (Replace x with t or i or n depending on your background)
We do this with things called "classes and objects". A class is a combination of data and functions which act on that data -- you can think of them like a type of variable (like int or float). An object is a specific instance of that variable type.
# by convention we capitalize a class name class CounterOne: # this function *must* be called __init__() def __init__(self): # note the "self"! self.x = 0 # note the "self" in the argument list! def get_next(self): # note the self.x y = self.x self.x = self.x+1 return y # note the capitalization (convention) counter = CounterOne() for i in range(8): print counter.get_next()
Silly example for using two classes:
import random class CounterOne: def __init__(self): self.x = 0 def get_next(self): y = self.x self.x = self.x+1 return y class CounterTwo: def __init__(self): self.x = 0 def get_next(self): y = self.x self.x = self.x+2 return y if random.random() < 0.5: counter = CounterOne() else: counter = CounterTwo() # the code here doesn't know if it's # using CounterOne or CounterTwo! for i in range(8): print counter.get_next()
More useful example -- read this one carefully!
class CounterOne: # this function *must* be called __init__() def __init__(self, increment_value): self.x = 0 self.increment_value = increment_value def get_next(self): y = 0.1 * (self.x) self.x = self.x + self.increment_value return y # pass in increment_value counter = CounterOne(4) for i in range(8): print counter.get_next()
import numpy # empty right now big_buffer = numpy.zeros(0) extra_stuff = numpy.zeros(2) big_buffer = numpy.append(big_buffer, extra_stuff) extra_stuff = numpy.zeros(2) big_buffer = numpy.append(big_buffer, extra_stuff) print big_buffer
Write a program that generates waves in buffers of 2048 samples.
scipy.io.wavfile.write(). This is because we have not covered how to write a wavfile incrementally -- you still must generate the waves in small bits.
Move on to Lab 4
Unless otherwise noted, all materials on these pages are licenced under a Creative Commons Licence.