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.

In [1]:
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.

In [2]:
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.

In [3]:
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#.

In [4]:
key = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
starting_mode = 'aeolian'
ending_mode = 'dorian'
In [5]:
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
In [6]:
print start_index, end_index
4 10
In [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 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]
Out[7]:
'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'
In [8]:
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)
In [9]:
tetris
Out[9]:
'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'
In [10]:
transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'dorian')
Out[10]:
'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:

In [11]:
import re
from IPython.display import Audio, display
In [12]:
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:

In [13]:
from IPython.html.widgets import interact, fixed
In [14]:
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())
        
Out[14]:
<function __main__.play_transposed_melody>

For rendering purposes, we're outputting the 7 modes below:

In [15]:
for mode in modes.keys():
    print mode
    play_transposed_melody(mode)
lydian
ionian