#!/usr/bin/env python """ \ ramid_ii.py -- Compose, play, and score a melody. Based on ramid.py version 1.0. """ from MidiOutFile import MidiOutFile from sys import argv, stderr import random from os import system from time import time from math import floor from random import * middle_C = 60 center = middle_C minor_3rd = 3 major_3rd = 4 fourth = 5 fifth = 7 octave = 12 # ================= pyramid structure stuff ================ class Strux: """ Song-structure-describing piece. It happens to resemble a C struct. Fields: start, stop, step -- params for its range() of positions in the song. invert (boolean) same (boolean) Used in the global "strux" array. """ pass strux = [[Strux()]] # strux[level][subpiece_number] = Strux() N_LEVELS = None def n_pieces( lvl ): return N_LEVELS + 1 - lvl def covers_sublevel( lvl ): covered = [ False ] * level_sz[ lvl-1 ] for p in range( n_pieces( lvl ) ): slp = strux[lvl][p] for i in range( slp.start, slp.stop, slp.step ): covered[i] = True return sum( covered ) == len( covered ) def create_strux( ): """ The global "strux" is a (straight-sided, not exponential) pyramid, each "piece" in each level just contains invert -- boolean (used by all the sequences) same -- boolean (not used by all sequences) start, stop, step -- range of (shared) pieces within lower row. strux shows structure-sharing. It does not enumerate all the note positions (but shows how to). It is not filled with information about notes. Once created, it does not get changed as the song details are composed. """ global strux, n_subpiece, piece_sz, level_sz strux = [ [ Strux() for i in range( n_pieces( lvl ) ) ] for lvl in range( N_LEVELS ) ] # Most phrases are built of two subphrases: n_subpiece = [ None ] + [ 2 ] * ( N_LEVELS - 1 ) # But sometimes there will be groups of three: if strux_rng.random() < .33: n_subpiece[ randrange( 1, 4 ) ] = 3 # Or even nine: if strux_rng.random() < .25: while True: i = randrange( 2, 5 ) if n_subpiece[ i ] != 3: n_subpiece[ i ] = 3 break piece_sz = [1] * N_LEVELS for lvl in range( 1, N_LEVELS ): piece_sz[lvl] = n_subpiece[lvl] * piece_sz[lvl-1] level_sz = [ n_pieces(lvl) * piece_sz[lvl] for lvl in range( N_LEVELS ) ] for lvl in range( 1, N_LEVELS ): while True: # until okay... for p in range( n_pieces( lvl ) ): n = n_pieces(lvl-1) - n_subpiece[lvl] + 1 q = strux_rng.randrange( n ) * piece_sz[lvl-1] slp = strux[lvl][p] slp.start = q slp.stop = q + piece_sz[lvl] slp.step = 1 if strux_rng.random() < .33: slp.start, slp.stop = slp.stop - 1, slp.start - 1 slp.step = -1 slp.invert = ( strux_rng.random() < .33 ) slp.same = ( strux_rng.random() < .33 ) if covers_sublevel( lvl ): break def print_the_pyramid(): for lvl in range( N_LEVELS - 1, -1, -1 ): for p in range( n_pieces(lvl) - 1, -1, -1 ): print >>stderr, strux[lvl][p].start, print >>stderr # ================== HOW TO MAKE SONGS ======================= mx_lvl_offs = fifth def randfrange( start, stop ): """ If start & stop are ints, it's like randrange(start,stop). If not, the fractional parts set odds for the top and bottom int in the range, in "the way you'd expect." """ return int( floor( random() * (stop-start) + start ) ) def randfint( first, last ): """ If first and last are ints, it's like randint( first, last ). If not, the fractional parts set odds for the top and bottom int in the range, in "the way you'd expect." """ return randfrange( first, last+1 ) def pyra_seq( center, mx_lvl_offs, invertible, sameable ): level_seq = [] # Level 0 level_seq.append( [] ) for i in range( level_sz[ 0 ] ): level_seq[0].append( center + randfint( -mx_lvl_offs, mx_lvl_offs ) ) for lvl in range( 1, N_LEVELS ): level_seq.append( [] ) for p in range( n_pieces(lvl) ): slp = strux[lvl][p] if slp.same and sameable: offs = 0 else: offs = randint( -mx_lvl_offs, mx_lvl_offs ) phrase = [] for i in range( slp.start, slp.stop, slp.step ): phrase.append( level_seq[lvl-1][i] + offs ) if slp.invert and invertible: t = min(phrase) + max(phrase) phrase = [ t - x for x in phrase ] level_seq[lvl] += phrase return level_seq[ -1 ] + [ level_seq[-1][0] ] # Repeat 1st note. def reconcirc( linears, circs, circnesses, lin_center ): """ \ Make a compromise between a linear, chromatic melody and sequence (circs) that moves around the circle of fifths. circnesses determine how much weight to give to circs vs. linears. """ notes = [] for i in range( len( linears ) ): min_d = None for n in range( linears[i]-6, linears[i]+6 ): d_lin = abs( n - linears[i] ) # Each chromatic step takes you 7 steps around the # circle of fifths: d_circ = abs( ( n * 7 - circs[i] ) % 12 - 6 ) d = (1.0 - circnesses[i]) * d_lin + circnesses[i] * d_circ if min_d == None or d < min_d: min_d = d closest = n notes.append( closest + lin_center ) return notes # "events" array: # each entry is a tuple ( # absolute time in msec, I_TIME = 0 # "on" or "off" I_ON_OFF = 1 # positional args array for m_note_on/off() (generally empty), I_POSARGS = 2 # keyword args dict for m_note_on/off() (channel, note, velocity) I_KWARGS = 3 # ) . # This lets the program plan note releases in the "far" future while # still generating note presses in the near present. Just sort by time # before playing. def cmp_by_note_then_time( event1, event2 ): """ Compare events for sorting by note value, then times of that note: """ time1, on_off1, posargs1, kwargs1 = event1 time2, on_off2, posargs2, kwargs2 = event2 return cmp( ( kwargs1[ "note" ], time1 ), ( kwargs2[ "note" ], time2 ) ) def fix_note_releases( events ): """ Fix problems with overlapping presses of the same note. If I press a note, then press it again, then release, the QuickTime MIDI player silences the note, even though the second press should still be sounding (or else the one note should still be sounding). Just dropping the first release seems to pile up sustained notes and cause problems. The solution here is to move the first release either before the second press, or delay it till the same time as the second release. Or something like that. events array is modified in place. """ events.sort( cmp_by_note_then_time ) i = 0 # First pass: delayed releases get their time set to None: while i < len( events ): i_start = i note = events[i][I_KWARGS]["note"] # Find all the on/off events for this note value: while i < len( events ) and events[i][I_KWARGS]["note"] == note: i += 1 i_stop = i # events[x] for i_start <= x < i_stop, are all the same note value. n_presses = 0 for j in range( i_start, i_stop ): time, on_off, posargs, kwargs = events[ j ] if on_off == "on": n_presses += 1 press_time = time else: if n_presses <= 0: raise( Exception( "note off without matching note on" ) ) if n_presses > 1: if ( j+1 > i_stop or time - press_time < events[j+1][I_TIME] - time ): # Put release before previous press: time = press_time - 1 else: time = None events[j] = ( time, ) + events[j][1:] n_presses -= 1 if ( events[ i_stop - 1 ][ I_TIME ] == None or events[ i_stop - 1 ][ I_ON_OFF ] == "on" ): raise( Exception( "note wasn't finished up" ) ) # Second pass: delayed releases get time from following release: next_release = events[ len(events)-1 ][ I_TIME ] for i in range( len(events)-2, -1, -1 ): if events[ i ][ I_TIME ] == None: events[i] = ( next_release, ) + events[i][1:] elif events[i][ I_ON_OFF ] == "off": next_release = events[i][ I_TIME ] def compose_song( song_seed ): global m, strux_rng, N_LEVELS, delay song_rng = Random( song_seed ) strux_rng = Random( song_rng.getrandbits( 64 ) ) # Seed the default rng: seed( song_rng.getrandbits( 64 ) ) N_LEVELS = 7 create_strux() # Now create a set of parallel sequences that follow the structure: # 3rd & 4th args are: invertible, sameable. (Everything is reversible). linears = pyra_seq( 0, fourth, True, True ) circs = pyra_seq( randrange(12), 1, False, True ) c_radius = randrange(25) # in percent c_center = 25 + randrange( c_radius/2 + 1 ) circnesses_pct = pyra_seq( c_center, c_radius/N_LEVELS, False, True ) circnesses = [ r*.01 for r in circnesses_pct ] # log delay in cents (1/1200 of an octave) relative to tempo logdelays = pyra_seq( 0, randrange(600)/N_LEVELS, False, False ) # log duration of note in cents relative to one beat s_min, s_max = -2400, 2400 s_radius = randrange( ( s_max - s_min ) / 2 ) s_center = randint( s_min + s_radius, s_max - s_radius ) # s_center = 300 logsustains = pyra_seq( s_center, s_radius/N_LEVELS, False, True ) notes = reconcirc( linears, circs, circnesses, center ) velocities = pyra_seq( 64, (48+randrange(15))/N_LEVELS, False, False ) tempo = randint( -1200/2, 1200/2 ) # Create a list of (future) events to be sorted by timestamps: # ( timestamp, on / off, positional_arguments, keyword_arguments ) # Off events sort before on events with the same time. events = [] # We come in with global delay = how long to wait to start the song. # We will ignore delay until we start to play the events below. time = 0 for i in range( len(notes) ): note_delay = int( 35 * 2**( (tempo + logdelays[i])/1200.0 ) ) sustain = int( note_delay * 2**( (logsustains[i])/1200.0 ) ) vel = velocities[i] note = notes[i] # Since the delay is associated with the note, not the gap between # notes, in order for a phrase to be reversible correctly, # the delay has to be symmetrical around the note! time += note_delay / 2 events.append( ( time, "on", [], { "channel": 1, "note": note, "velocity": velocities[i] } ) ) events.append( ( time + sustain, "off", [], { "channel": 1, "note": note } ) ) time += note_delay / 2 fix_note_releases( events ) # for i in range(20): # print events[i] # quit() # PLAY THE NOTES: events.sort() # First event time may be negative, so: prev_time = events[0][0] - delay for time, off_on, pos_args, kw_args in events: m.update_time( time - prev_time ) prev_time = time if off_on == "on": m.note_on( *pos_args, **kw_args ) else: m.note_off( *pos_args, **kw_args ) # We leave at last note_off. # On exit, value of the global variable "delay" is ignored. return notes # ======================== START THE MIDI! ====================== def mkname( i ): alphabet = "ABCDEFGJKLMNPQRTUVWXYcdefghijkmnopqrstuvwxyz346789" i = int( abs(i) + 1 ) # so it can't be zero, so at least one digit name = "" while i > 0: x, i = i % len(alphabet), i / len(alphabet) name = alphabet[ x ] + name return name prog_name = argv[0] album_name = "" out_file = "" playp = False showp = False while argv: arg = argv.pop(0) if arg in [ "-p", "--play" ]: playp = True elif arg in [ "-s", "--show" ]: showp = True else: # out_file = arg album_name = arg.split('.')[ 0 ] if album_name == "": short_name = prog_name.split('/')[-1].split('\\')[-1] album_name = short_name.split('.')[ 0 ] + "_" + mkname( time() * 2**20 ) if out_file == "": out_file = album_name+".mid" m = MidiOutFile(out_file) # non optional midi framework m.header() m.start_of_track() m.sequence_name(album_name) m.instrument_name('Piano') delay = 0 notes = compose_song( album_name ) # delay = 120 # for if there is a next song, else ignored # non optional midi framework m.update_time(240) m.end_of_track() m.eof() print "out_file =", out_file if playp: system( "open " + out_file ) if showp: from abjad import * from spianostaff import StevePianoStaff abjad_notes = [ Note( pitch - middle_C, (1,8) ) for pitch in notes ] score = StevePianoStaff( (4,4), abjad_notes ) score.show( title=album_name )