Last active
August 29, 2015 13:59
-
-
Save solarmist/10567427 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# algorithmic tone row composer | |
# Python 3 only | |
# Copyright 2014 Chris Muggli-Miller | |
# [email protected] | |
# Free for personal use | |
# Do not redistribute without my permission | |
# Composes a 12-tone row using every tone in the Western chromatic scale | |
# exactly once. Instead of choosing each note at random, the program is given | |
# an awareness of the preceeding melodic interval, and it decides the next note | |
# based on that interval. The result is that the row sounds unusually melodic | |
# (tone rows normally sound dissonant), but it still adheres to the rule of | |
# using every note once and only once. | |
# I think of this as computer-assisted composing, not pure algorithmic | |
# composing, because the program doesn't have any say over the rules. All it | |
# does is give us a list of notes based on a predetermined set of rules. | |
# Another way to think of it is that the program is really just displacing some | |
# tedious calculations that a human would otherwise need to do. | |
from random import randint # very important for decision-making... | |
# First, create a list of the display names for all of the notes. | |
ALL_TONES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'] | |
# Here, we have to keep track of each note by marking it '1' so that the | |
# same note never gets used twice. Each position in the USED_TONES list | |
# corresponds to the respective note name in the ALL_TONES list. | |
# 0 = not yet used, 1 = already used | |
USED_TONES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | |
# This function checks whether the desired note is available. If it is, it | |
# returns the desired note. If not, it randomly chooses a new note. It will | |
# keep trying until it finds an available note. | |
def get_new_note(desired_pitch): | |
while USED_TONES[desired_pitch] == 1: | |
# can't re-use a pitch, so pick a new one | |
desired_pitch = randint(0, 11) | |
return desired_pitch | |
# This function moves from one pitch to another based on the chosen interval. | |
# An interval (for our purposes) is just a number specifying by how many | |
# semitones the pitch should be moved. I.e., an interval of '2' means return a | |
# new pitch that is two semitones higher than the given pitch. | |
def move_by_interval(pitch_class, interval): | |
pitch_class += interval # move the pitch by 'interval' amount of semitones | |
# wrap the new pitch into range, if necessary | |
# (move it up or down by one octave so that it always falls between 0-11) | |
if pitch_class < 0: | |
pitch_class += 12 | |
elif pitch_class > 11: | |
pitch_class -= 12 | |
return pitch_class | |
def main(): | |
# Next, these two variables keep track of the last two notes that were | |
# chosen so that we can determine the interval that was between them. | |
# -1 means the note is yet undefined; valid range is 0-11 | |
previous_note = -1 | |
previous_note2 = -1 | |
# start composing a tone row! | |
# pick a first pitch at random (musically, it doesn't matter what note | |
# comes first) | |
current_note = randint(0, 11) | |
USED_TONES[current_note] = 1 | |
# print each note as we choose it - this is how we display the row to | |
# the user | |
print(ALL_TONES[current_note], end=' ') | |
previous_note = current_note | |
# pick a 2nd pitch | |
# Now we start to see some logic. Choose from the following list of | |
# pleasant-sounding intervals to determine the 2nd note. This way, the | |
# first interval in the row will always sound pleasing and never dissonant. | |
# (This rule is based on my musical opinion, and it doesn't have to be | |
# this way necessarily.) | |
intervaltype = randint(1, 4) | |
if intervaltype == 1: # use a major 3rd | |
interval = 4 | |
elif intervaltype == 2: # minor 3rd | |
interval = 3 | |
elif intervaltype == 3: # perfect 4th | |
interval = 5 | |
elif intervaltype == 4: # perfect 5th | |
interval = 7 | |
# store the last two chosen notes | |
previous_note2 = previous_note | |
previous_note = current_note | |
current_note = move_by_interval(previous_note, interval) | |
USED_TONES[current_note] = 1 | |
# display the 2nd note we chose above | |
print(ALL_TONES[current_note], end=' ') | |
# now pick the next 10 pitches | |
# Here the decision-making logic is minimal. We could use more than this, | |
# but it turns out this works surprisingly well. We actually don't want TOO | |
# much logic or else the rules become too rigid and the program will | |
# compose the same thing every time. | |
for i in range(10): | |
# keep record of the last two chosen notes... | |
previous_note2 = previous_note | |
previous_note = current_note | |
# get the interval of the last two notes | |
previous_interval = previous_note2 - previous_note | |
# intelligently choose a new pitch based on the interval between the | |
# previous two | |
if previous_interval == 4: | |
chance = randint(1, 4) # roll a 4-sided die, basically | |
if chance == 1: | |
# a 1-in-4 chance of skipping the logic and choosing randomly | |
current_note = get_new_note(randint(0, 11)) | |
USED_TONES[current_note] = 1 | |
else: | |
# if the previous interval was a minor 3rd, attempt to follow | |
# it with a major 2nd | |
current_note = get_new_note(move_by_interval(previous_note, 3)) | |
USED_TONES[current_note] = 1 | |
elif previous_interval == 5: | |
chance = randint(1, 4) # roll the die | |
if chance == 1: | |
current_note = get_new_note(randint(0, 11)) | |
USED_TONES[current_note] = 1 | |
else: | |
# if the previous interval was a major 3rd, attempt to follow | |
# by going down a minor 3rd | |
current_note = get_new_note(move_by_interval(previous_note, | |
-4)) | |
USED_TONES[current_note] = 1 | |
else: | |
# if all else fails, just pick randomly | |
current_note = get_new_note(randint(0, 11)) | |
USED_TONES[current_note] = 1 | |
print(ALL_TONES[current_note], end=' ') | |
print('\nEnd of row!') | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment