Week 14: Generating Music#

Laboratory 9
Last updated November 28, 2023

00. Content #

Mathematics

  • N/A

Programming Skills

  • Functions

  • Arrays

Embedded Systems

  • N/A

Write your name and email below:

Name:

Email:

1. Music Generation from Random Processes #

In this lab, we will use random processes to automatically generate music.

So we will now move from the concept of random variable to the concept of random process, which we define below.

Definitions:

A (discrete-time) random process is a sequence of random variables $X_n$, where $n$ ranges over a finite or countable number of integers. Another name for “random process” is “stochastic process.” You may also encounter the term “time series.” A time series is a sample from a random process. In other works, a time series is a set of values $x_n$, where $x_n$ is a sample (i.e., a set value) of the random variable $X_n$.

Let us construct a very simple piece of music at random.

The piece of music will be in the “C Major” tonality (i.e. the key of C Major), which means that the notes that we play will be restricted to the notes of the C Major scale, which is composed of the following sequence of notes:

import numpy as np
import pandas as pd

notes = "C C# D D# E F F# G G# A A# B"
scale = pd.Series(440*2**((np.arange(12)-9)/12), index = notes.split())


summary = pd.DataFrame({
    'Frequency': scale,
})
summary
Frequency
C 261.625565
C# 277.182631
D 293.664768
D# 311.126984
E 329.627557
F 349.228231
F# 369.994423
G 391.995436
G# 415.304698
A 440.000000
A# 466.163762
B 493.883301

The following code will play a note at a frequency of 262 Hz for 1 second.

from IPython.display import Audio


def get_sine_wave(frequency, duration, sample_rate=44100, amplitude=4096):
    t = np.linspace(0, duration, int(sample_rate*duration))
    wave = amplitude*np.sin(2*np.pi*frequency*t)
    return wave


wave = get_sine_wave(262, 1)

Audio(wave, rate = 44100)

Exercise 1#

Modify this code to play a tune consisting of sequence of 20 notes with the same duration, where each note is randomly chosen following a uniform distribution on the 8 notes of the C Major scale.

Which of the results that you produced is a time series? Which one is a random process?

Each of the tunes you generated is a time series. The process used to generate it is a random (stochastic) process. You can reuse the same process to generate a different music (different time series, but the process would be the same.)

Exercise 2#

Modify the probability distribution in the previous exercise to try to improve your tune. Listen to the tunes played by other students. Is there a probability distribution that sounds more pleasing? (hint: try to play the 1st, 3rd, and 5th note most often, and increase the probability of the 4th note slightly over that of the others.)

What is the probability mass function corresponding to the tune you played ?

The probability mass function is a vector containing the probability of playing each note. The sum of the entries of the vector should equal to 1.

Now we will use two dimensional random variables to add rhythm to the tune.

Exercise 3#

Create a sequence of 20 two dimensional random variables (\(X_n\) ,\(Y_n\) ), for \(n\)=1,…,20, where \(X_n\) is an integer between 1 and 8 representing the note, and \(Y_n\) is a fraction (either ¼, ½, or 1) representing the duration of the note.

Question: What is the probability mass function you used for each \(X_n\) ? What is the probability mass function you used for each \(Y_n\) ? What is the joint probability mass function you used for the two dimensional random variables (\(X_n\),\(Y_n\))? Are \(X_n\) and \(Y_n\) independent in your model?

Adding Rhythm - Patterns

A more natural sounding way to add rhythm is to pick among short rhythm patterns. For example, one can pick (e.g., uniformly) from the following rhythm patterns:

  • Rhythm Pattern 1: 1 note lasting 1 second

  • Rhythm Pattern 2: 2 notes per second, each lasting one half second

  • Rhythm Pattern 3: 3 notes per second- each lasting one third of a second

  • Rhythm Pattern 4: 4 notes per second – each lasting one quarter of a second

  • Rhythm Pattern 5: 1 note for half a second, followed by 2 notes each lasting a quarter of a second.

Exercise 4#

Pick the rhythm pattern at random, and assign the notes in the pattern at random, to play a song in the C Major scale.

Combining note patterns and rhythms patterns

Define a number of two-note patterns, for example:

  • two-note patterns 1: Note, Note+1

  • two-note patterns 2: Note, Note+2

  • two-note patterns 3: Note, Note+4

Similarly, define a number of three-note patterns and 4 note patterns, for example:

  • three-note patterns 1: Note, Note+1, Note+2

  • three-note patterns 2: Note, Note+2, Note+4

  • four-note patterns 1: Note, Note+1, Note+2, Note+3

  • four-note patterns 2: Note, Note+2, Note, Note+4

Exercise 5#

Create a tune by picking values of a series of 20 two-dimensional random variables (\(X_n\),\(Y_n\)) Where \(X_n\) is the Rhythm pattern and \(Y_n\) is the Note pattern that fits \(X_n\).

Are \(X_n\) and \(Y_n\) independent?

In the previous exercise, the note pattern was fixed. Now let us try to generate the note patterns at random using a transition matrix.

Exercise 6#

The given code uses a transition matrix for generating musical notes (all of the same duration).

Run the code and explain how we are generating random sequences of music. (Hint: random.choices() function)

This code uses mingus and midiutil which can be installed from terminal by using: pip install < package> or conda install < package>.

MIDI is the shortform for Music Instrument Digital Interface. It is a standard industrial format for creating digital music. This is the file format used by Artists all over the world to create there music at the moment using applications like Logic Pro, Garageband etc. midiutil library is a popular library for writing music in python. It provides a simple way to add notes to a music track by just specifing the note, its durarion and amplitude.

The resulting midi file can be played using vlc player, garageband, logic pro etc.

If for some reason this code is causing issues, I have added a more compatible version in the next code block. (P.S the MIDI version sounds the best).

Usage of Mingus will be explained below.

#imports
import numpy as np
import random

from mingus.core import chords
from midiutil import MIDIFile


keys = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
#major_7th_chords = ['CM7', 'C#M7', 'DM7', 'EbM7', 'EM7', 'FM7', 'F#M7', 'GM7', 'AbM7', 'AM7', 'BbM7', 'BM7']
key_map = {"C": 0, "C#":1, "D":2, "Eb":3, "E":4, "F":5, "F#":6, "G":7, "Ab":8, "A":9, "Bb":10, "B":11}

def standardize_notation(note):
    
    if note == 'A#':
        note = 'Bb'
    elif note == 'E#':
        note = 'F'
    elif note == 'Gb':
        note = 'F#'
    elif note == 'G#':
        note = 'Ab'
    elif note == 'B#':
        note = 'C'
    elif note == 'Db':
        note = 'C#'
    elif note == 'D#':
        note = 'Eb'

    return note

# here we are using midi-util library to generate MIDI which is shortform for Music Instrument Digital Interface.
# For working with the library we need to convert the notes into numbers.

def get_midi_representation_of_note(note, octave):
    
    note = standardize_notation(note)
    assert(octave <= 12 and octave >= 0)

    note = key_map[note]
    note += (12 * octave)

    assert(note <= 127 and note >= 0)

    return note

transition_matrix = np.zeros((12, 12))


#create transition matrix. This Transition matrix is made assuming all the chords are Major 7 chords
for key in keys:
    
    #major seventh chord
    chord = key + "M7"
    preferred_keys = chords.from_shorthand(chord)
    
    major_chord_pos = key_map[key]
    
    assert(len(preferred_keys) == 4)
            
    for i in range(3):
        transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[i])]] = 0.3
        
    transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[3])]] = 0.1
print("******************************************************************************")
print("--------------------- T R A N S I T I O N    M A T R I X ---------------------")
                          
print(transition_matrix)
print("******************************************************************************")

#major 7 chords
chord_progression = ["F", "C", "F", "G"]

notes = []

for chord in chord_progression:
    chord = standardize_notation(chord)
    
    key_probabilities_for_chord = transition_matrix[key_map[chord]]
    
    # play 4 notes for each chord
    for i in range(4):
        notes.extend(random.choices(keys, weights=key_probabilities_for_chord, k=1))
print("******************************************************************************")
print("------------------------------------ N O T E S -------------------------------")

print(notes)

print("******************************************************************************")

t = 0  # In beats
dur = 1  # In beats
tempo = 120  # In BPM
vol = 100  # 0-127, as per the MIDI standard

MyMIDI = MIDIFile(1) 
MyMIDI.addTempo(0, t, tempo)


for i, note in enumerate(notes):
    # get note number
    pitch = get_midi_representation_of_note(note = note, octave = 4)
    
    # adding individual note to the track
    MyMIDI.addNote(track = 0, channel = 0, pitch = pitch, time = t + i, duration = dur, volume = vol)



with open("midi1.mid", "wb") as output_file:
    MyMIDI.writeFile(output_file)
        
    
******************************************************************************
--------------------- T R A N S I T I O N    M A T R I X ---------------------
[[0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1]
 [0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0. ]
 [0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0. ]
 [0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0. ]
 [0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3]
 [0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0. ]
 [0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0. ]
 [0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3]
 [0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0. ]
 [0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0. ]
 [0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0. ]
 [0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3]]
******************************************************************************
******************************************************************************
------------------------------------ N O T E S -------------------------------
['A', 'F', 'C', 'C', 'E', 'E', 'G', 'B', 'A', 'C', 'C', 'F', 'G', 'D', 'D', 'D']
******************************************************************************
notes = "A Bb B C C# D Eb E F F# G Ab"


full_scale = {}
for i in range(1, 7):
    for j, letter in enumerate(notes.split()):
        full_scale[letter + str(i)] = 440 * 2 ** (i - 4 + j / 12)
full_scale


#imports
import numpy as np
import random
from IPython.display import Audio

from mingus.core import chords


keys = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
#major_7th_chords = ['CM7', 'C#M7', 'DM7', 'EbM7', 'EM7', 'FM7', 'F#M7', 'GM7', 'AbM7', 'AM7', 'BbM7', 'BM7']
key_map = {"C": 0, "C#":1, "D":2, "Eb":3, "E":4, "F":5, "F#":6, "G":7, "Ab":8, "A":9, "Bb":10, "B":11}

def standardize_notation(note):
    
    if note == 'A#':
        note = 'Bb'
    elif note == 'E#':
        note = 'F'
    elif note == 'Gb':
        note = 'F#'
    elif note == 'G#':
        note = 'Ab'
    elif note == 'B#':
        note = 'C'
    elif note == 'Db':
        note = 'C#'
    elif note == 'D#':
        note = 'Eb'

    return note

def get_sine_wave(frequency, duration, sample_rate=44100, amplitude=4096):
    t = np.linspace(0, duration, int(sample_rate*duration))
    wave = amplitude*np.sin(2*np.pi*frequency*t)
    return wave


transition_matrix = np.zeros((12, 12))


#create transition matrix. Transition matrix is made assuming all the chords are Major 7 chords
for key in keys:
    
    #major seventh chord
    chord = key + "M7"
    preferred_keys = chords.from_shorthand(chord)
    
    major_chord_pos = key_map[key]
    
    assert(len(preferred_keys) == 4)
            
    for i in range(3):
        transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[i])]] = 0.3
        
    transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[3])]] = 0.1
                          
print("******************************************************************************")
print("--------------------- T R A N S I T I O N    M A T R I X ---------------------")
                          
print(transition_matrix)
print("******************************************************************************")

#major 7 chords
chord_progression = ["F", "C", "F", "G"]

notes = []

for chord in chord_progression:
    chord = standardize_notation(chord)
    
    key_probabilities_for_chord = transition_matrix[key_map[chord]]
    
    # play 4 notes for each chord
    for i in range(4):
        notes.extend(random.choices(keys, weights=key_probabilities_for_chord, k=1))

print("******************************************************************************")
print("------------------------------------ N O T E S -------------------------------")

print(notes)

print("******************************************************************************")

track1 = []


for i, note in enumerate(notes):
    
    frequency = full_scale[note+"4"]
    track1.extend(get_sine_wave(frequency = frequency, duration = 1, sample_rate=44100, amplitude=4096))
    

Audio(track1, rate = 44100)
******************************************************************************
--------------------- T R A N S I T I O N    M A T R I X ---------------------
[[0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1]
 [0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0. ]
 [0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0.  0. ]
 [0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3 0. ]
 [0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0.  0.3]
 [0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0.  0. ]
 [0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3 0. ]
 [0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0.  0.3]
 [0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0.  0. ]
 [0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0.  0. ]
 [0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3 0. ]
 [0.  0.  0.  0.3 0.  0.  0.3 0.  0.  0.  0.1 0.3]]
******************************************************************************
******************************************************************************
------------------------------------ N O T E S -------------------------------
['E', 'F', 'A', 'C', 'E', 'C', 'G', 'B', 'A', 'A', 'C', 'A', 'D', 'D', 'D', 'G']
******************************************************************************

Mingus Python Package

Mingus is an advanced, cross-platform music theory and notation package for Python with MIDI file and playback support.

Python code to get the note combinations in a chord:

from mingus.core import chords

# To find the notes in C Major chord.
result = chords.from_shorthand("Cmaj")

print(result)
['C', 'E', 'G']

Playing chords

Chords are played throughout a piece of music to create the foundation of a tune.
The basic (major) chords consist of a root note and two other notes, namely “root note+2” and “root note+4”. So there is a total of 3 notes in a (major) chord. For example, the C (major) chord consists of C, E, and G.

The 3 notes of a basic (major) chord can be played all together (adding the sound waves together), as an arpeggio (e.g. CEGECEG…), or in any other sequence (e.g. CEGCEGCEG… or CCCEGCCCEGCCCEG… etc.) There are many other, more complicated possibilities, including adding other notes here and there, but these are some of the basic ways to play an arpeggio.

Use mingus to find out the components of a given chord if you do not know.

Exercise 7#

Play all 3 notes of the C chord together for 1 second, and subsequently play them in an arpeggio pattern (or other pattern of your choice).

  • Repeat with the G chord.

  • Repeat with the F chord.

Sequences of chords

A simple model for generating music is based on generating a sequence of Chords. There are many ways to generate the sequence of chords. One simple way to set the key (e.g., C key) and then to play at random the chord with root note equal to either the first, fourth, and firth note in the scale of the key. For example, in the C key, one could pick at random (e.g., uniformly) to play either the C chord, the F chord, or the G chord.

Use mingus to find out the components of a given chord if you do not know.

Exercise 8#

Play a sequence of chords (notes played together) in the C key following the probability model

  • C chord probability =2/5

  • G chord probability =2/G

  • F chord probability =1/5.

Adding melody to a chord sequence

Given a sequence of chords, one can generate music by adapting the notes played to the chord played at the time. For example, one could prioritize (high probablity) 1st, 3rd, 4th, 5th notes of the scale of the root of the chord, over the other notes of the scale.

Exercise 9#

Play a sequence of chord (randomly generates) in the C key (notes played together). Then add a melody using a probability model for the notes that is chord dependent. For example, when playing the C chord, the notes could be restricted to C,D,E (uniformly) and for the F chord, the notes could be restricted to (FAC). Add a rhythm to your melody.

Exercise 10#

Pick your own chord, use code given to play music that goes well with it.

#imports
import numpy as np
import random

from mingus.core import chords
from midiutil import MIDIFile


keys = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
#major_7th_chords = ['CM7', 'C#M7', 'DM7', 'EbM7', 'EM7', 'FM7', 'F#M7', 'GM7', 'AbM7', 'AM7', 'BbM7', 'BM7']
key_map = {"C": 0, "C#":1, "D":2, "Eb":3, "E":4, "F":5, "F#":6, "G":7, "Ab":8, "A":9, "Bb":10, "B":11}

def standardize_notation(note):
    
    if note == 'A#':
        note = 'Bb'
    elif note == 'E#':
        note = 'F'
    elif note == 'Gb':
        note = 'F#'
    elif note == 'G#':
        note = 'Ab'
    elif note == 'B#':
        note = 'C'
    elif note == 'Db':
        note = 'C#'
    elif note == 'D#':
        note = 'Eb'

    return note

def get_midi_representation_of_note(note, octave):
    
    note = standardize_notation(note)
    assert(octave <= 12 and octave >= 0)

    note = key_map[note]
    note += (12 * octave)

    assert(note <= 127 and note >= 0)

    return note



transition_matrix = np.zeros((12, 12))


#create transition matrix. Transition matrix is made assuming all the chords are Major 7 chords
for key in keys:
    
    #major seventh chord
    chord = key + "M7"
    preferred_keys = chords.from_shorthand(chord)
    
    major_chord_pos = key_map[key]
    
    assert(len(preferred_keys) == 4)
            
    for i in range(3):
        transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[i])]] = 0.3
        
    transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[3])]] = 0.1
                          
print("******************************************************************************")
print("--------------------- T R A N S I T I O N    M A T R I X ---------------------")
                          
print(transition_matrix)
print("******************************************************************************")


#add custom chords here
#major 7 chords
chord_progression = ["F", "C", "F", "G"]

notes = []

for chord in chord_progression:
    chord = standardize_notation(chord)
    
    key_probabilities_for_chord = transition_matrix[key_map[chord]]
    
    # play 4 notes each chord
    for i in range(4):
        notes.extend(random.choices(keys, weights=key_probabilities_for_chord, k=1))

print("******************************************************************************")
print("------------------------------------ N O T E S -------------------------------")

print(notes)

print("******************************************************************************")

t = 0  # In beats
dur = 1  # In beats
tempo = 120  # In BPM
vol = 100  # 0-127, as per the MIDI standard

MyMIDI = MIDIFile(1) 
MyMIDI.addTempo(0, t, tempo)


for i, note in enumerate(notes):
    pitch = get_midi_representation_of_note(note = note, octave = 4)
    
    MyMIDI.addNote(track = 0, channel = 0, pitch = pitch, time = t + i, duration = dur, volume = vol)

for i, note in enumerate(chord_progression):
    pitch = get_midi_representation_of_note(note = note, octave = 3)
    
    MyMIDI.addNote(track = 0, channel = 1, pitch = pitch, time = t + (i*4), duration = dur, volume = vol)

with open("latest.mid", "wb") as output_file:
    MyMIDI.writeFile(output_file)
        
    

More Compatible Version of the Code Above:

notes = "A Bb B C C# D Eb E F F# G Ab"


full_scale = {}
for i in range(1, 7):
    for j, letter in enumerate(notes.split()):
        full_scale[letter + str(i)] = 440 * 2 ** (i - 4 + j / 12)
full_scale


#imports
import numpy as np
import random
from IPython.display import Audio

from mingus.core import chords


keys = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
#major_7th_chords = ['CM7', 'C#M7', 'DM7', 'EbM7', 'EM7', 'FM7', 'F#M7', 'GM7', 'AbM7', 'AM7', 'BbM7', 'BM7']
key_map = {"C": 0, "C#":1, "D":2, "Eb":3, "E":4, "F":5, "F#":6, "G":7, "Ab":8, "A":9, "Bb":10, "B":11}

def standardize_notation(note):
    
    if note == 'A#':
        note = 'Bb'
    elif note == 'E#':
        note = 'F'
    elif note == 'Gb':
        note = 'F#'
    elif note == 'G#':
        note = 'Ab'
    elif note == 'B#':
        note = 'C'
    elif note == 'Db':
        note = 'C#'
    elif note == 'D#':
        note = 'Eb'

    return note

def get_sine_wave(frequency, duration, sample_rate=44100, amplitude=4096):
    t = np.linspace(0, duration, int(sample_rate*duration))
    wave = amplitude*np.sin(2*np.pi*frequency*t)
    return wave


transition_matrix = np.zeros((12, 12))


#create transition matrix. Transition matrix is made assuming all the chords are Major 7 chords
for key in keys:
    
    #major seventh chord
    chord = key + "M7"
    preferred_keys = chords.from_shorthand(chord)
    
    major_chord_pos = key_map[key]
    
    assert(len(preferred_keys) == 4)
            
    for i in range(3):
        transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[i])]] = 0.3
        
    transition_matrix[major_chord_pos, key_map[standardize_notation(preferred_keys[3])]] = 0.1
                          
print("******************************************************************************")
print("--------------------- T R A N S I T I O N    M A T R I X ---------------------")
                          
print(transition_matrix)
print("******************************************************************************")

#major 7 chords
chord_progression = ["F", "C", "F", "G"]

notes = []

for chord in chord_progression:
    chord = standardize_notation(chord)
    
    key_probabilities_for_chord = transition_matrix[key_map[chord]]
    
    # play 4 notes each chord
    for i in range(4):
        notes.extend(random.choices(keys, weights=key_probabilities_for_chord, k=1))

print("******************************************************************************")
print("------------------------------------ N O T E S -------------------------------")

print(notes)

print("******************************************************************************")

track1 = []
track2 = []

for i, note in enumerate(notes):
    
    frequency = full_scale[note+"4"]
    track1.extend(get_sine_wave(frequency = frequency, duration = 1, sample_rate=44100, amplitude=4096))
    
for i, note in enumerate(chord_progression):
    frequency = full_scale[note+"3"]
    track2.extend(get_sine_wave(frequency = frequency, duration = 1, sample_rate=44100, amplitude=2048))
    track2.extend(np.zeros(3*44100))


wave = np.add(track1, track2)

Audio(wave, rate = 44100)

Reflection #

  1. Which part of the lab did you find the most challenging?

  2. Which part of the lab was the easiest?

  3. Please list any other comments below.

Write Answers for the Reflection Below