The seven modes of Tetris
In this post, our goal is to transpose Tetris to all seven modes of the major scale and play it with the help of the Game Boy sound that we have developed in a previous post.
For those not familiar with the major scale and its modes, I will not attempt an explication here. Please look it up on Wikipedia as it is a complicated notion.
Transposing to a different mode¶
First, we're going to define the modes of the major scale we will use.
from collections import OrderedDict
modes = OrderedDict()
for mode_name, alterations in zip(['lydian', 'ionian', 'mixolydian', 'dorian', 'aeolian', 'phrygian', 'locrian'],
[[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 1, 1, 1, 1, 1, 1]]):
modes[mode_name] = alterations
Then, we define the melody (Tetris in our case) in the Nokia melody format.
tetris = "4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f6,4a6,8g6,8f6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5"
Now, we import the tools we will use.
from pylab import *
from scipy.signal import square
Let's process the melody step by step. Our test case is that we want to go from aeolian, in which the melody is written, to phrygian. This means that only one note changes: b becomes a#.
key = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
starting_mode = 'aeolian'
ending_mode = 'dorian'
mode_names = modes.keys()
start_index = mode_names.index(starting_mode)
end_index = mode_names.index(ending_mode)
if end_index < start_index:
end_index += 7
print start_index, end_index
4 10
transposition = array([0, 0, 0, 0, 0, 0, 0])
for i in range(start_index, end_index):
transposition += array(modes[mode_names[i % 7]])
print transposition
note_scale = ["a", "a#", "b", "c", "c#", "d", "d#", "e", "f", "f#", "g", "g#"]
transposed_melody = []
for note in tetris.split(','):
if note[-1] == 'p':
transposed_melody.append(note)
else:
for target_note in note_scale:
if note.find(target_note) != -1:
duration, octave = note.split(target_note)
break
transposed_melody.append(
duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)
",".join(transposed_melody)
[0 0 0 0 0 1 0]
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f#6,4a6,8g6,8f#6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'
def transpose(melody, key, starting_mode, ending_mode):
mode_names = modes.keys()
start_index = mode_names.index(starting_mode)
end_index = mode_names.index(ending_mode)
if end_index < start_index:
end_index += 7
transposition = array([0, 0, 0, 0, 0, 0, 0])
for i in range(start_index, end_index):
transposition += array(modes[mode_names[i % 7]])
#print transposition
note_scale = ["a", "a#", "b", "c", "c#", "d", "d#", "e", "f", "f#", "g", "g#"]
transposed_melody = []
for note in melody.split(','):
if note[-1] == 'p':
transposed_melody.append(note)
else:
for target_note in note_scale:
if note.find(target_note) != -1:
duration, octave = note.split(target_note)
break
transposed_melody.append(
duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)
return ",".join(transposed_melody)
tetris
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f6,4a6,8g6,8f6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'
transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'dorian')
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f#6,4a6,8g6,8f#6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'
Playing the transposed melody with a Game Boy sound¶
From our previous post, we know how to play this sort of ringtone with the code below:
import re
from IPython.display import Audio, display
def play_melody(melody, sample_freq=10.e3, bpm=50):
duration = re.compile("^[0-9]+")
pitch = re.compile("[\D]+[\d]*")
measure_duration = 4 * 60. / bpm #usually it's 4/4 measures
output = zeros((0,))
for note in melody.split(','):
# regexp matching
duration_match = duration.findall(note)
pitch_match = pitch.findall(note)
# duration
if len(duration_match) == 0:
t_max = 1/4.
else:
t_max = 1/float(duration_match[0])
if "." in pitch_match[0]:
t_max *= 1.5
pitch_match[0] = "".join(pitch_match[0].split("."))
t_max = t_max * measure_duration
# pitch
if pitch_match[0] == 'p':
freq = 0
else:
if pitch_match[0][-1] in ["4", "5", "6", "7"]: # octave is known
octave = ["4", "5", "6", "7"].index(pitch_match[0][-1]) + 4
height = pitch_match[0][:-1]
else: # octave is not known
octave = 5
height = pitch_match[0]
freq = 261.626 * 2 ** ((["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"].index(height) / 12. + octave - 4))
# generate sound
t = arange(0, t_max, 1/sample_freq)
wave = square(2 * pi * freq * t)
# append to output
output = hstack((output, wave))
display(Audio(output, rate=sample_freq))
Using this rudimentary appartus, we can now listen to what this sounds like:
from IPython.html.widgets import interact, fixed
def play_transposed_melody(mode):
transposed_melody = transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode)
#print transposed_melody
play_melody(transposed_melody, bpm=130)
interact(play_transposed_melody,
mode=modes.keys())
<function __main__.play_transposed_melody>
For rendering purposes, we're outputting the 7 modes below:
for mode in modes.keys():
print mode
play_transposed_melody(mode)
lydian
ionian
mixolydian
dorian
aeolian
phrygian
locrian
Bonus: pitch-shifting the result with the bowl sound from Zulko¶
First, we copy Zulko's code for pitch-shifting.
import numpy as np
def speedx(snd_array, factor):
""" Multiplies the sound's speed by some `factor` """
indices = np.round( np.arange(0, len(snd_array), factor) )
indices = indices[indices < len(snd_array)].astype(int)
return snd_array[ indices.astype(int) ]
def stretch(sound_array, f, window_size, h):
""" Stretches the sound by a factor `f` """
phase = np.zeros(window_size)
hanning_window = np.hanning(window_size)
result = np.zeros( len(sound_array) /f + window_size)
for i in np.arange(0, len(sound_array)-(window_size+h), h*f):
# two potentially overlapping subarrays
a1 = sound_array[i: i + window_size]
a2 = sound_array[i + h: i + window_size + h]
# resynchronize the second array on the first
s1 = np.fft.fft(hanning_window * a1)
s2 = np.fft.fft(hanning_window * a2)
phase = (phase + np.angle(s2/s1)) % 2*np.pi
a2_rephased = np.fft.ifft(np.abs(s2)*np.exp(1j*phase))
# add to result
i2 = int(i/f)
result[i2 : i2 + window_size] += hanning_window*a2_rephased
result = ((2**(16-4)) * result/result.max()) # normalize (16bit)
return result.astype('int16')
def pitchshift(snd_array, n, window_size=2**13, h=2**11):
""" Changes the pitch of a sound by ``n`` semitones. """
factor = 2**(1.0 * n / 12.0)
stretched = stretch(snd_array, 1.0/factor, window_size, h)
return speedx(stretched[window_size:], factor)
Now, we generate the sounds we're going to need:
from scipy.io import wavfile
fps, bowl_sound = wavfile.read("../../../Pianoputer/bowl.wav")
tones = range(-25,25)
transposed = [pitchshift(bowl_sound, n) for n in tones]
-c:22: ComplexWarning: Casting complex values to real discards the imaginary part
print fps
48000
These sounds can be plugged in the note generation process.
def play_melody_with_bowl(melody, sample_freq=10.e3, bpm=50):
duration = re.compile("^[0-9]+")
pitch = re.compile("[\D]+[\d]*")
measure_duration = 4 * 60. / bpm #usually it's 4/4 measures
output = zeros((0,))
for note in melody.split(','):
# regexp matching
duration_match = duration.findall(note)
pitch_match = pitch.findall(note)
# duration
if len(duration_match) == 0:
t_max = 1/4.
else:
t_max = 1/float(duration_match[0])
if "." in pitch_match[0]:
t_max *= 1.5
pitch_match[0] = "".join(pitch_match[0].split("."))
t_max = t_max * measure_duration
# pitch
if pitch_match[0] == 'p':
freq = 0
else:
if pitch_match[0][-1] in ["4", "5", "6", "7"]: # octave is known
octave = ["4", "5", "6", "7"].index(pitch_match[0][-1]) + 4
height = pitch_match[0][:-1]
else: # octave is not known
octave = 5
height = pitch_match[0]
sound_index = (["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"].index(height) + (octave - 5) * 12)
# generate sound
t = arange(0, t_max, 1./sample_freq)
wave = transposed[sound_index]
wave = wave[:t.size]
# append to output
output = hstack((output, wave))
display(Audio(output, rate=sample_freq))
A simple test below will show us if our program works:
play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'aeolian'), sample_freq=fps, bpm=120.)
And now, to finish this post, please enjoy the seven modes playing Tetris with a bowl sound courtesy of the amazing Zulko's blog:
for mode in modes.keys():
print mode
play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode), sample_freq=fps, bpm=120.)
lydian
ionian
mixolydian
dorian
aeolian
phrygian
locrian
This post was entirely written using the IPython notebook. You can see a static view or download this notebook with the help of nbviewer at 20140917_SevenModesOfTetris.ipynb.