Back to main

Lab 3 for C programming

Exercises:

Exercise 8: Arrays

Background

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 numpy (numerical python) module. Note that these arrays are zero-indexed.

creates an array in memory, storing each value at the appropriate index:

Index0123
Value3-298712

Technical details

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()

Your task...

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.

... show your work to a demonstrator


Exercise 9: Reading and writing wav files

Background

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 the wavfile.

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.

Technical details

Reading and writing a file. Download a440-1second.wav and place it in the same directory as your .py file.

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)

Your task...

Two steps to this exercise:

  1. Write a program which asks the user for a frequency, then generates a wavfile with a 1-second sine wave at that frequency.
  2. Modify your previous program so that it asks the user for multiple sine waves. Keep on asking for more frequencies until the user types a 0.
    Once you have all the frequencies, "mix" them together to create a single 1-second wavfile containing sine waves of those frequencies at once.

... show your work to a demonstrator


Exercise 10: Classes and buffer-based audio

Background

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.

Technical details

Classes:

# 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()

Appending arrays:

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

Your task...

Write a program that generates waves in buffers of 2048 samples.

... show your work to a demonstrator

Move on to Lab 4


Creative Commons LicenseUnless otherwise noted, all materials on these pages are licenced under a Creative Commons Licence.