CS 198: Project #7

### Project 7: Too Many Notes

Project 7 is to make use of python to create sounds that consist of hundreds of notes, without having to write more than a few lines of code. Csound is very powerful, but to take advantage of it you need to be able to generate notes algorithmically. You can't create hundreds of notes for each small time period by hand.

### Creating Notes

1. A Csound file is just a text file, which means it's a sequence of characters. Let's assume we have a very simple instrument with one oscillator and a linear envelope to shape the sound.
```; one oscillator, fast rise, long decay
instr   1
k1   linen   1, 0.05*p3, p3, 0.7*p3
a1   oscil   p4*k1, p5, 1
out     a1
endin
```

A single note for this instrument looks like the following, which plays a 440 Hz note for 1 second at an amplitude of 10000.

```; sin wave table
f	1	0	8192	10	1

; notes
i    1     0.00     1.00      10000   440.0
```

Using MacCsound, create an instrument and score as above and test it out.

2. If I wanted to create 100 notes that all played simultaneously at slightly different frequencies, it would be nice if I could write a python function to do it for me. Creating such a file by hand would be tedious, at best. Fortunately, generating strings and writing them to a file is something we can do quite easily in Python.

Opening a file and writing the above string to it is as simple as the following three lines of code, which write one line of text and a carriage return '\n' to the file notefile.csd.

```fp = file( 'notefile.csd', 'w')
fp.write( 'i    1     0.00     1.00      10000   440.0\n' )
fp.close()
```

But what if we wanted to write 100 notes to that file? We need a loop, we need a way of generating random values, and we need to be able to insert numbers into a string. All three are easy to do.

```import random

fp = file( 'notefile.txt', 'w')
for i in range(100):  # loop 100 times
pitch = str( 440.0 + (random.random() - 0.5)*100 )
volume = str( 10000 / 100 )
fp.write( 'i    1     0.00     1.00      ' + volume + '   ' + pitch + '\n' )
fp.close()
```

The function random.random() generates a number between 0 and 1. If we subtract 0.5 from it and multiply the result by 100, then we get random numbers between -50 and 50. Adding these to 440 and gives us a range of pitches from 390 to 490.

To make the total volume add up to a particular value we need to calculate the volume of each individual note. The second line in the for loop takes care of that.

In the line that writes the string to the file, the addition of strings just concatenates them together.

In TextWrangler, create a python file out of the code above and run it. Then open up the notefile.txt file in TextWrangler and copy and paste the notes into the MacCsound score. The score should have one line that begins with an f to create the sin wave table and then the list of notes. Play the note.

3. What if we wanted to parameterize the frequency and the range? We should create a function to create and write out the notes. Then we could call the function with different frequency and range values. We should also parameterize the start time and duration. Your function might look like the following.
```def makenotes( fp, start, duration, volume, pitch, pitchRange, N ):

for i in range(N):  # loop N times

pitchStr = str( pitch + (random.random() - 0.5)*pitchRange )
volumeStr = str( volume / 100 )
startStr = str(start)
durationStr = str(duration)

fp.write( 'i    1     ' + startStr + '     ' + durationStr + '      ' + volumeStr + '   ' + pitchStr + '\n' )
```

After creating the above function, we can call it with different parameters and generate many more notes across time.

```fp = file( 'notefile.txt', 'w')
makenotes( fp, 0, 2, 12000, 220, 100, 100 )
makenotes( fp, 2, 2, 12000, 440, 100, 100 )
makenotes( fp, 4, 2, 12000, 880, 100, 100 )
fp.close()
```

Again, copy and past from notefile.txt into the MacCSound score.

4. What if we wanted to make a sound that was a whole series of note clusters with random base pitches that plays quickly? We should be able to do with by putting calls to makenotes into a loop.
```start = 0
duration = 0.15
for i in range(100):
pitch = random.randint( 50, 4000 )
pitchRange = pitch/2
makenotes( fp, start, duration, 15000, pitch, pitchRange, 100 )
start += duration
```

Copy the notes and play the sequence of sounds.

5. Create a few 10s sounds. Try using makenotes with different parameter sets to see how different numbers of notes sound. There are several different random distributions you can use, including a Gaussian distribution (normal distribution). For example:
```pitchStr = str( random.gauss( pitchBase, pitchRange ) )
```