Skip to content

Instantly share code, notes, and snippets.

@solarmist
Last active August 29, 2015 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save solarmist/10567427 to your computer and use it in GitHub Desktop.
Save solarmist/10567427 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# algorithmic tone row composer
# Python 3 only
# Copyright 2014 Chris Muggli-Miller
# rontasu@gmail.com
# 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