"""

   Generation of drum beats by division, a la Arbeau, kind of.   
   
   Creates an "abc" file that is then converted to MIDI by
   abc2midi and played by timidity (with eawpatches). 

      
   Drum out a beat,
   Build up a sweat and a frenzy, 
   Open up a channel to some inner infinity, and
   Let it pour out,
   Cry out in rage against the darkness,
   Boldly declare "I am a god!",
   Childishly mistaken.
      
"""


import os, random

f = open('drum.abc','wt')

# ABC Song header
print >> f, """\
X:1
T:Divisions
M:1/1
Q:25
K:C
%%MIDI program 115"""

initial = 'c 116'
scale = ['C','G','D','A','E','B']
instruments = [ '13', '113', '115', '116', '117' ]


def left_depth(division):
    if not isinstance(division, str):
        if len(division) == 1:
            return left_depth(division)
        else:
            return left_depth(division)+1
    return 0
    
def divide(division, cache, depth_limit=2):
    """ Elaborate a division """
    if not isinstance(division, str):
        if len(division) == 1:
            return divide(division[0], cache, depth_limit)
        
        return [ divide(division[0], cache, depth_limit-1) ] + \
               [ divide(item, cache) for item in division[1:] ]

    if depth_limit <= 0:
        return division
    
    if depth_limit > left_depth(division) and random.random() < 0.5:
       return [ division ] * random.choice([2,3])
    
    if division not in cache:
        i = scale.index(division[0].upper())
        instr = division.split()[1]
        core = scale[max(0,i-1):min(len(scale),i+2)]   
        pool = [ ]
        for item in core:
            for instr in instruments:
                pool.append(item + ' '+instr)
                pool.append(item + ', '+instr)
                pool.append(item.lower() + ' '+instr)
        
        pool = [ random.choice(pool)
                 for i in xrange(4) ]
        cache[division] = [
            [ random.choice(pool) for i in xrange(random.choice([1,2,3])) ]
            for j in xrange(random.choice([1,2,3]))
        ]

    return random.choice( cache[division] )

def check(division, depth_limit=2):
    if not isinstance(division, str):
        if len(division) == 1:
            return check(division[0], depth_limit)

        if not check(division[0], depth_limit-1): return False
        for item in division[1:]:
            if not check(item): return False
        return True

    return depth_limit >= 0

last_note = 'z 116'
last_vol = 64
def show_note(note, divisor, volume):
    """ Ass-backwards note output,
        since we want to specify the delay *before* the note,
        not after. """
    global last_note, last_vol
    last_note_note, last_note_instrument = last_note.split()
    print >> f, '%%MIDI program', last_note_instrument    
    print >> f, '%%%%MIDI beat %d %d %d 1' % (last_vol,last_vol,last_vol)
    print >> f, '%s/%d|' % (last_note_note, divisor)
    last_note = note
    last_vol = volume

def render_division(division, divisor, volume=1.0, volume2=1.0):
    if isinstance(division, str):
        show_note(division, divisor, int(volume*127))
    else:
        f = 0.75
        for part in division[:-1]:
            render_division(part, divisor*len(division), volume2*f, volume2*f*f)
        render_division(division[-1], divisor*len(division), volume, volume2*f)

def norm_form(x):
    """ Used to avoid repeating ourselves """
    
    if isinstance(x,str):
        return None
    if len(x) == 1:
        return norm_form(x[0])
    return tuple([ norm_form(item) for item in x ])

seen = { }
def new_division(d):
    while True:
        new_d = divide(d, {})
        if check(new_d) and norm_form(new_d) not in seen:
            seen[norm_form(new_d)] = True
            return new_d

def subsong(d, depth):
    """ Generate a set of elaborations
        on a beat pattern. """
    print d
    seen[norm_form(d)] = True   

    render_division(d,1,0.9,0.9)
    render_division(d,1)
    
    if depth >= 0:
        d1 = new_division(d)
        d2 = new_division(d)        
        for i, subd in [(2,d1),(1,d2),(1,d1)]:
            subsong(subd, depth-i)

    render_division(d,1)
    
subsong(initial, 2)

# Flush final beat
show_note('z 116',1,64)

f.close()

os.system(
  'abc2midi drum.abc >/dev/null && timidity -A200 drum1.mid'
)

"""
timidity -A200 drum1.mid -Ow -o drum.wav
lame drum.wav
"""
