#!/usr/bin/env python """ \ ramid.py -- Loose C translation of pyracirc. 0.7 is the first version that got the linear/circular mixing going. Separate structure description. """ from MidiOutFile import MidiOutFile from sys import argv, stderr import random from os import system from time import time from random import * prog_name = argv[0].split('.') if len( argv ) > 1: out_file = argv[ 1 ] else: out_file = prog_name[0]+".mid" # "" means stdout to this stuff middle_C = 60 center = middle_C minor_3rd = 3 major_3rd = 4 fourth = 5 fifth = 7 octave = 12 # ================= pyramid structure stuff ================ class Strux: pass # an empty C struct-like thing to attach fields to 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( ): 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 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 + randint( -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 def sing_a_song( song_seed=None ): global file_rng, strux_rng if song_seed == None: song_seed = file_rng.randrange( 2*31 ) song_rng = Random( song_seed ) strux_rng = Random( song_rng.randrange( 2**31 ) ) # Seed the default rng: seed( song_rng.randrange( 2**31 ) ) global n_levels, delay 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_range = randrange(25) # in percent c_center = 25 + randrange( c_range/2 + 1 ) c_nesses_pct = pyra_seq( c_center, c_range/n_levels, False, True ) circnesses = [ r*.01 for r in c_nesses_pct ] # log delay in cents (1/1200 of an octave) relative to tempo logdelays = pyra_seq( 0, randrange(1200)/n_levels, False, False ) notes = reconcirc( linears, circs, circnesses, center ) velocities = pyra_seq( 64, (32+randrange(31))/n_levels, False, False ) tempo = randint( -1200/2, 1200/2 ) n_simult = 2 for i in range( len(notes) + n_simult + 1 ): # In order for phrases to be reversible, delay has to be # symmectrical around the note! if i < len(notes): note_delay = int( 35 * 2 ** ( (tempo+logdelays[i])/1200.0 ) ) else: note_delay = 60 delay += note_delay / 2 m.update_time( delay ) delay = 0 if 0 <= ( i - n_simult ) < len(notes): m.note_off( channel=1, note=notes[ i - n_simult ] ) m.update_time( 0 ) if i < len(notes): m.note_on( channel=1, note=notes[i], velocity=velocities[i] ) delay += note_delay / 2 # ======================== START THE MIDI! ====================== m = MidiOutFile(out_file) # non optional midi framework m.header() m.start_of_track() m.sequence_name(prog_name[0]) m.instrument_name('Piano') file_seed = int( time() ) file_rng = Random( file_seed ) delay = 0 for s in range( 60 ): sing_a_song() delay += 120 # non optional midi framework m.update_time(240) m.end_of_track() m.eof() system( "open " + out_file )