{ "metadata": { "name": "", "signature": "sha256:f6599446350033cc4ea52b32155acfa1bb7fbcc64658da3f63ad1288cf2103de" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "For those not familiar with the major scale and its modes, I will not attempt an explication here. Please look it up on [Wikipedia](http://en.wikipedia.org/wiki/Major_scale) as it is a complicated notion." ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Transposing to a different mode" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we're going to define the modes of the major scale we will use." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from collections import OrderedDict\n", "\n", "modes = OrderedDict()\n", "for mode_name, alterations in zip(['lydian', 'ionian', 'mixolydian', 'dorian', 'aeolian', 'phrygian', 'locrian'],\n", " [[0, 0, 0, -1, 0, 0, 0],\n", " [0, 0, 0, 0, 0, 0, -1],\n", " [0, 0, -1, 0, 0, 0, 0],\n", " [0, 0, 0, 0, 0, -1, 0],\n", " [0, -1, 0, 0, 0, 0, 0],\n", " [0, 0, 0, 0, -1, 0, 0],\n", " [0, 1, 1, 1, 1, 1, 1]]):\n", " modes[mode_name] = alterations\n", "\n" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then, we define the melody (Tetris in our case) in the Nokia melody format." ] }, { "cell_type": "code", "collapsed": false, "input": [ "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\"" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we import the tools we will use." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from scipy.signal import square" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "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#." ] }, { "cell_type": "code", "collapsed": false, "input": [ "key = ['a', 'b', 'c', 'd', 'e', 'f', 'g']\n", "starting_mode = 'aeolian'\n", "ending_mode = 'dorian'" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "mode_names = modes.keys()\n", "start_index = mode_names.index(starting_mode)\n", "end_index = mode_names.index(ending_mode)\n", "if end_index < start_index:\n", " end_index += 7" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "print start_index, end_index" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "4 10\n" ] } ], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "transposition = array([0, 0, 0, 0, 0, 0, 0])\n", "for i in range(start_index, end_index):\n", " transposition += array(modes[mode_names[i % 7]])\n", "print transposition\n", "\n", "note_scale = [\"a\", \"a#\", \"b\", \"c\", \"c#\", \"d\", \"d#\", \"e\", \"f\", \"f#\", \"g\", \"g#\"]\n", "transposed_melody = []\n", "for note in tetris.split(','):\n", " if note[-1] == 'p':\n", " transposed_melody.append(note)\n", " else:\n", " for target_note in note_scale:\n", " if note.find(target_note) != -1:\n", " duration, octave = note.split(target_note)\n", " break\n", " transposed_melody.append(\n", " duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)\n", "\",\".join(transposed_melody)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[0 0 0 0 0 1 0]\n" ] }, { "metadata": {}, "output_type": "pyout", "prompt_number": 7, "text": [ "'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'" ] } ], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "def transpose(melody, key, starting_mode, ending_mode):\n", " mode_names = modes.keys()\n", " start_index = mode_names.index(starting_mode)\n", " end_index = mode_names.index(ending_mode)\n", " if end_index < start_index:\n", " end_index += 7\n", " transposition = array([0, 0, 0, 0, 0, 0, 0])\n", " for i in range(start_index, end_index):\n", " transposition += array(modes[mode_names[i % 7]])\n", " #print transposition\n", " note_scale = [\"a\", \"a#\", \"b\", \"c\", \"c#\", \"d\", \"d#\", \"e\", \"f\", \"f#\", \"g\", \"g#\"]\n", " transposed_melody = []\n", " for note in melody.split(','):\n", " if note[-1] == 'p':\n", " transposed_melody.append(note)\n", " else:\n", " for target_note in note_scale:\n", " if note.find(target_note) != -1:\n", " duration, octave = note.split(target_note)\n", " break\n", " transposed_melody.append(\n", " duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)\n", " return \",\".join(transposed_melody)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 8 }, { "cell_type": "code", "collapsed": false, "input": [ "tetris" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 9, "text": [ "'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'" ] } ], "prompt_number": 9 }, { "cell_type": "code", "collapsed": false, "input": [ "transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'dorian')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 10, "text": [ "'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'" ] } ], "prompt_number": 10 }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Playing the transposed melody with a Game Boy sound" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From our previous post, we know how to play this sort of ringtone with the code below:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import re\n", "from IPython.display import Audio, display" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 11 }, { "cell_type": "code", "collapsed": false, "input": [ "def play_melody(melody, sample_freq=10.e3, bpm=50):\n", " duration = re.compile(\"^[0-9]+\")\n", " pitch = re.compile(\"[\\D]+[\\d]*\") \n", " measure_duration = 4 * 60. / bpm #usually it's 4/4 measures\n", " output = zeros((0,))\n", " for note in melody.split(','):\n", " # regexp matching\n", " duration_match = duration.findall(note)\n", " pitch_match = pitch.findall(note)\n", " \n", " # duration \n", " if len(duration_match) == 0:\n", " t_max = 1/4.\n", " else:\n", " t_max = 1/float(duration_match[0])\n", " if \".\" in pitch_match[0]:\n", " t_max *= 1.5\n", " pitch_match[0] = \"\".join(pitch_match[0].split(\".\"))\n", " t_max = t_max * measure_duration\n", " \n", " # pitch\n", " if pitch_match[0] == 'p':\n", " freq = 0\n", " else:\n", " if pitch_match[0][-1] in [\"4\", \"5\", \"6\", \"7\"]: # octave is known\n", " octave = [\"4\", \"5\", \"6\", \"7\"].index(pitch_match[0][-1]) + 4 \n", " height = pitch_match[0][:-1]\n", " else: # octave is not known\n", " octave = 5\n", " height = pitch_match[0]\n", " freq = 261.626 * 2 ** (([\"c\", \"c#\", \"d\", \"d#\", \"e\", \"f\", \"f#\", \"g\", \"g#\", \"a\", \"a#\", \"b\"].index(height) / 12. + octave - 4)) \n", " \n", " # generate sound\n", " t = arange(0, t_max, 1/sample_freq)\n", " wave = square(2 * pi * freq * t)\n", " \n", " # append to output\n", " output = hstack((output, wave))\n", " \n", " display(Audio(output, rate=sample_freq)) " ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 12 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using this rudimentary appartus, we can now listen to what this sounds like:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from IPython.html.widgets import interact, fixed" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 13 }, { "cell_type": "code", "collapsed": false, "input": [ "def play_transposed_melody(mode): \n", " transposed_melody = transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode)\n", " #print transposed_melody\n", " play_melody(transposed_melody, bpm=130)\n", "\n", "interact(play_transposed_melody,\n", " mode=modes.keys())\n", " " ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "metadata": {}, "output_type": "pyout", "prompt_number": 14, "text": [ "" ] } ], "prompt_number": 14 }, { "cell_type": "markdown", "metadata": {}, "source": [ "For rendering purposes, we're outputting the 7 modes below:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for mode in modes.keys():\n", " print mode\n", " play_transposed_melody(mode)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "lydian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "ionian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "mixolydian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "dorian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "aeolian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "phrygian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "locrian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 15 }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Bonus: pitch-shifting the result with the bowl sound from Zulko" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we copy [Zulko's code](http://zulko.github.io/blog/2014/03/29/soundstretching-and-pitch-shifting-in-python/) for pitch-shifting." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "\n", "def speedx(snd_array, factor):\n", " \"\"\" Multiplies the sound's speed by some `factor` \"\"\"\n", " indices = np.round( np.arange(0, len(snd_array), factor) )\n", " indices = indices[indices < len(snd_array)].astype(int)\n", " return snd_array[ indices.astype(int) ]" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 16 }, { "cell_type": "code", "collapsed": false, "input": [ "def stretch(sound_array, f, window_size, h):\n", " \"\"\" Stretches the sound by a factor `f` \"\"\"\n", "\n", " phase = np.zeros(window_size)\n", " hanning_window = np.hanning(window_size)\n", " result = np.zeros( len(sound_array) /f + window_size)\n", "\n", " for i in np.arange(0, len(sound_array)-(window_size+h), h*f):\n", "\n", " # two potentially overlapping subarrays\n", " a1 = sound_array[i: i + window_size]\n", " a2 = sound_array[i + h: i + window_size + h]\n", "\n", " # resynchronize the second array on the first\n", " s1 = np.fft.fft(hanning_window * a1)\n", " s2 = np.fft.fft(hanning_window * a2)\n", " phase = (phase + np.angle(s2/s1)) % 2*np.pi\n", " a2_rephased = np.fft.ifft(np.abs(s2)*np.exp(1j*phase))\n", "\n", " # add to result\n", " i2 = int(i/f)\n", " result[i2 : i2 + window_size] += hanning_window*a2_rephased\n", "\n", " result = ((2**(16-4)) * result/result.max()) # normalize (16bit)\n", "\n", " return result.astype('int16')" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 17 }, { "cell_type": "code", "collapsed": false, "input": [ "def pitchshift(snd_array, n, window_size=2**13, h=2**11):\n", " \"\"\" Changes the pitch of a sound by ``n`` semitones. \"\"\"\n", " factor = 2**(1.0 * n / 12.0)\n", " stretched = stretch(snd_array, 1.0/factor, window_size, h)\n", " return speedx(stretched[window_size:], factor)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 18 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we generate the sounds we're going to need:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from scipy.io import wavfile\n", "\n", "fps, bowl_sound = wavfile.read(\"../../../Pianoputer/bowl.wav\")\n", "tones = range(-25,25)\n", "transposed = [pitchshift(bowl_sound, n) for n in tones]" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stderr", "text": [ "-c:22: ComplexWarning: Casting complex values to real discards the imaginary part\n" ] } ], "prompt_number": 19 }, { "cell_type": "code", "collapsed": false, "input": [ "print fps" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "48000\n" ] } ], "prompt_number": 20 }, { "cell_type": "markdown", "metadata": {}, "source": [ "These sounds can be plugged in the note generation process." ] }, { "cell_type": "code", "collapsed": false, "input": [ "def play_melody_with_bowl(melody, sample_freq=10.e3, bpm=50):\n", " duration = re.compile(\"^[0-9]+\")\n", " pitch = re.compile(\"[\\D]+[\\d]*\") \n", " measure_duration = 4 * 60. / bpm #usually it's 4/4 measures\n", " output = zeros((0,))\n", " for note in melody.split(','):\n", " # regexp matching\n", " duration_match = duration.findall(note)\n", " pitch_match = pitch.findall(note)\n", " \n", " # duration \n", " if len(duration_match) == 0:\n", " t_max = 1/4.\n", " else:\n", " t_max = 1/float(duration_match[0])\n", " if \".\" in pitch_match[0]:\n", " t_max *= 1.5\n", " pitch_match[0] = \"\".join(pitch_match[0].split(\".\"))\n", " t_max = t_max * measure_duration\n", " \n", " # pitch\n", " if pitch_match[0] == 'p':\n", " freq = 0\n", " else:\n", " if pitch_match[0][-1] in [\"4\", \"5\", \"6\", \"7\"]: # octave is known\n", " octave = [\"4\", \"5\", \"6\", \"7\"].index(pitch_match[0][-1]) + 4 \n", " height = pitch_match[0][:-1]\n", " else: # octave is not known\n", " octave = 5\n", " height = pitch_match[0]\n", " sound_index = ([\"c\", \"c#\", \"d\", \"d#\", \"e\", \"f\", \"f#\", \"g\", \"g#\", \"a\", \"a#\", \"b\"].index(height) + (octave - 5) * 12) \n", " \n", " # generate sound\n", " t = arange(0, t_max, 1./sample_freq)\n", " wave = transposed[sound_index]\n", " wave = wave[:t.size]\n", " \n", " # append to output\n", " output = hstack((output, wave))\n", " \n", " display(Audio(output, rate=sample_freq)) " ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 23 }, { "cell_type": "markdown", "metadata": {}, "source": [ "A simple test below will show us if our program works:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'aeolian'), sample_freq=fps, bpm=120.)" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 24 }, { "cell_type": "markdown", "metadata": {}, "source": [ "And now, to finish this post, please enjoy the seven modes playing Tetris with a bowl sound courtesy of the amazing Zulko's blog:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for mode in modes.keys():\n", " print mode\n", " play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode), sample_freq=fps, bpm=120.)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "lydian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "ionian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "mixolydian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "dorian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "aeolian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "phrygian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] }, { "output_type": "stream", "stream": "stdout", "text": [ "locrian\n" ] }, { "html": [ "\n", " \n", " " ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 25 }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](http://nbviewer.ipython.org/urls/raw.github.com/flothesof/posts/master/20140917_SevenModesOfTetris.ipynb)." ] } ], "metadata": {} } ] }