!pip install music21
import itertools
import random
from music21 import * # python musicology
import numpy as np # used essentially to transpose arrays for plotting
import matplotlib.pyplot as plt # plot cellular automata
random.seed(487638) # to make sure random processes are consistent between runs
def create_ca1D(length, init, rules):
"""
Generates a cellular automata array of n rows from initial conditions and a rule set defined as a list of tuples generating ones.
Arguments:
length: positive integer describing the length of the cellular automata (number of simulations)
init: a list of zeros and ones whose length specifies to the width of the cellular automata
rules: a list of tuples or lists specifying a pattern in the previous state generating a one in the current state
Returns:
a 2D array of zeros and ones describing the cellular automata
"""
ncells = len(init)
array = [[0] * ncells for _ in range(length)] # * crée des copies
array[0] = init
for i in range(1, length):
for j in range(ncells):
if j != (ncells - 1):
ca_at = (array[i-1][j-1], array[i-1][j], array[i-1][j+1])
else:
ca_at = (array[i-1][j-1], array[i-1][j], array[i-1][0])
if ca_at in rules:
array[i][j] = 1
return(array)
def strip_ca1D(array, limits):
"""
Selects a strip in a cellular automata array.
Arguments:
array: a list of equal-length lists of zeros and ones describing the cellular automata (usually the output of the `create_ca1D()` function)
limits: a list of lists of 2 items describing the limits of the strips in the width of `array`
Returns:
a list of 2D arrays of zeros and ones describing cellular automata
"""
trim_indices = list(range(limits[0], limits[1]))
array_trim = []
for i in range(len(array)):
trim_i = []
for j in trim_indices:
trim_i.append(array[i][j])
array_trim.append(trim_i)
return(array_trim)
def ca1D_to_stream(array, notes, durations, key_ = None):
"""
Maps a cellular automata array to notes and durations.
Arguments:
array: a list of equal-length lists of zeros and ones describing the cellular automata (usually the output of the `stip_ca1D()` function)
notes: a list of notes in the form of "C3", "A5", "F4", etc., equal to the width (second dimension) of `array`
durations: a list of durations in quarter-lengths
key_: Optional; A key signature to add to the stream
Returns:
a music21 stream
"""
nevents = len(array)
ncells = len(array[0])
durations_iterator = itertools.cycle(durations)
stream_ca = stream.Stream()
if key_ is not None:
stream_ca.keySignature = key.Key(key_)
for i in range(nevents):
notes_i = []
duration_i = next(durations_iterator)
for j in range(ncells):
if array[i][j] == 1:
notes_i.append(notes[j])
if len(notes_i) == 0:
stream_ca.append(note.Rest(quarterLength = duration_i))
elif len(notes_i) == 1:
stream_ca.append(note.Note(notes_i[0], quarterLength = duration_i))
else:
stream_ca.append(chord.Chord(notes_i, quarterLength = duration_i))
return(stream_ca)
def chords_to_notes(stream_, select = 'lowest'):
"""
Scans a music21 stream to replace chords by a single note of the chord (lowest, highest or random).
Arguments:
stream_: a music21 stream
select: a string among ['lowest', 'highest', 'random'] describing which note in the chord will replace the chord
Returns:
a music21 stream
"""
chordless = stream.Stream()
for i in stream_.iter().notesAndRests:
if type(i) == type(chord.Chord()):
if select == 'highest':
n = i.notes[-1]
elif select == 'lowest':
n = i.notes[0]
elif select == 'random':
n = random.sample(i.notes, k = 1)
else:
print("select must be 'high', 'low' or 'random'")
else:
n = i
chordless.append(n)
return(chordless)
ca1D_rules = dict({
'018': [(1, 0, 0), (0, 0, 1)], # the one used in the video
'022': [(1, 0, 0), (0, 1, 0), (0, 0, 1)],
'030': [(1, 0, 0), (0, 1, 1), (0, 1, 0), (0, 0, 1)],
'045': [(1, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 0)],
'054': [(1, 0, 1), (1, 0, 0), (0, 1, 0), (0, 0, 1)],
'060': [(1, 0, 1), (1, 0, 0), (0, 1, 1), (0, 1, 0)],
'073': [(1, 1, 0), (0, 1, 1), (0, 0, 0)],
'102': [(1, 1, 0), (1, 0, 1), (0, 1, 0), (0, 0, 1)],
'105': [(1, 1, 0), (1, 0, 1), (0, 1, 1), (0, 0, 0)],
'110': [(1, 1, 0), (1, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 1)],
'126': [(1, 1, 0), (1, 0, 1), (1, 0, 0), (0, 1, 1), (0, 1, 0), (0, 0, 1)],
'150': [(1, 1, 1), (1, 0, 0), (0, 1, 0), (0, 0, 1)]
}) # 256 rule sets http://atlas.wolfram.com/01/01/
width = 50 # height of domains
length = 24 # length of domains
init = [0] * width # initial condition with only zeroes, but...
init[width // 2] = 1 # altered with a one (or true, or turned on) cell in the middle
fig, axes = plt.subplots(2, 6, figsize = (10, 5))
fig.subplots_adjust(hspace=0.5)
axes = axes.flatten()
for i, rule in enumerate(ca1D_rules.keys()):
ca_i = create_ca1D(length, init, ca1D_rules[rule])
axes[i].matshow(np.array(ca_i).transpose(), cmap ="binary")
axes[i].invert_yaxis()
axes[i].set_title('rule ' + rule)
axes[i].axis('off')
# Generate cells
ca1_rule = ca1D_rules['150']
width1 = 200
length1 = 136
init = [0] * width1
init[width1 // 2] = 1
ca1 = create_ca1D(length1, init, ca1_rule)
# select portions of the grid
trimlimits1 = [
[width1//2 - 3, width1//2 + 3], # guitar
[width1//2 - 15, width1//2 - 8] # bass guitar
]
# plot
fig, ax = plt.subplots(figsize = (5, 5))
ax.matshow(np.array(ca1).transpose(), cmap ="binary")
ax.invert_yaxis()
ax.tick_params(axis='both', labelsize=10)
for i in range(len(trimlimits1)):
rect = plt.Rectangle(
(0, trimlimits1[i][0]),
width = length1,
height = trimlimits1[i][1] - trimlimits1[i][0],
color = '#38A0A790'
)
ax.add_patch(rect)
ax.arrow(x=5, y=126, dx=0, dy=-15, width=0.5)
ax.arrow(x=5, y=65, dx=0, dy=15, width=0.5)
ax.text(2, 129, "Guitar", fontsize=10)
ax.text(2, 58, "Bass", fontsize=10);
# Generate cells
ca2_rule = ca1D_rules['018']
width2 = 200
length2 = 136
init = random.choices([0, 1], weights=[0.9, 0.1], k=width2)
ca2 = create_ca1D(length2, init, ca2_rule)
# select portions of the grid
trimlimits2 = [
[110, 116], # drum kit
]
# plot
fig, ax = plt.subplots(figsize = (5, 5))
ax.matshow(np.array(ca2).transpose(), cmap ="binary")
ax.invert_yaxis()
ax.tick_params(axis='both', labelsize=10)
rect = plt.Rectangle(
(0, trimlimits2[0][0]),
width = length2,
height = trimlimits2[0][1] - trimlimits2[0][0],
color = '#38A0A790'
)
ax.add_patch(rect);
array1_trim = strip_ca1D(ca1, limits = trimlimits1[0])
array2_trim = strip_ca1D(ca1, limits = trimlimits1[1])
array3_trim = strip_ca1D(ca2, limits = trimlimits2[0])
arrays_trim = [array1_trim, array2_trim, array3_trim]
fig, axs = plt.subplots(len(arrays_trim), 1, figsize = (12, 5))
for i, a in enumerate(arrays_trim):
axs[i].matshow(np.array(a).transpose(), cmap ="binary")
axs[i].invert_yaxis()
axs[i].set_ylabel('track ' + str(i+1), rotation=0, labelpad=30)
# Guitar
track1 = ca1D_to_stream(
array = array1_trim,
notes = ['C4', 'D4', 'Eb4', 'F4', 'G4', 'Ab4', 'Bb4'],
durations = [0.5, 0.5, 1, 2, 1, 1, 0.5, 1.5]
)
# Bass
track2 = ca1D_to_stream(
array = array2_trim,
notes = ['C3', 'D3', 'Eb3', 'F3', 'G3', 'Ab3', 'Bb3'],
durations = [1, 1, 2, 0.5, 0.5, 0.5, 0.5, 2]
)
# Drums
track3 = ca1D_to_stream(
array = array3_trim,
notes = ['C3', 'D3', 'Eb3', 'F3', 'G3', 'Ab3', 'Bb3'],
durations = [2, 1, 1, 0.5, 0.5, 1, 1, 1]
).transpose('P5')
track2.show('midi')
track2_chordless = chords_to_notes(track2, select='random')
piece = stream.Stream()
piece.insert(track1)
piece.insert(track2_chordless)
piece.insert(track3)
piece.show('midi')
piece.write('midi', 'piece.midi')