#!/usr/bin/python
# MOD file synchronizer, 2008-11-20 by Nick Fankhauser
# Synchronizes movement of sprites to MOD channels
# Uses timelapse as background for the sprites
# Requires: mencoder, ffmpeg, lame, timidity

import sys,os
abspeed=0.02
spritedir='/home/nyk/ideas/Person/'
bgdir='/home/nyk/ideas/unm/'
fade_nb=10
debug=False # write angle of sprite into image
sprite_scaling=1.5
waiting_turns=20 # must be high enough to allow object to escape from border zone in low angles

def empty_directory(d):
 for f in os.listdir(d):
  os.unlink('%s/%s' % (d.rstrip('/'),f))

commands=['','Portamento Up','Portamento Down','TonePortamento','Vibrato','ToneP + VolSlide','Vibra + VolSlide','Tremolo','* NOT USED *','SampleOffset','VolumeSlide','PositionJump','Set Volume','PatternBreak','Misc. Cmds','Set Speed']

def get_pattern(data,p,speed,fps):
 samples,notes,effects,lastchan={},{},{},{}
 tots=0
 last_speed=speed
 for i in range(64):
  tots+=abspeed*speed
  frame=int(tots*fps)
  for chan in range(4):
   note=''
   for byte in range(4):
    v=ord(data[1084+p*1024+i*16+chan*4+byte])
    b=bin(v)[2:]
    note+=(8-len(b))*'0'+b
   sample=int(note[:4]+note[16:20],2)
   note_period=int(note[4:16],2)
   effect_command=int(note[20:24],2)
   effect_data=int(note[24:],2)
   if effect_command==15: speed=effect_data
   if not sample and lastchan.has_key(chan): sample=lastchan[chan] # better?
   lastchan[chan]=sample
   if speed!=last_speed:
    tots-=abspeed*last_speed
    tots+=abspeed*speed
    frame=int(tots*fps)
   last_speed=speed
   samples[frame]=samples.get(frame,[])+[sample]
   notes[frame]=notes.get(frame,[])+[note_period]
   effects[frame]=effects.get(frame,[])+[effect_command]
 samplist,notelist,efflist=[],[],[]
 for i in range(max(samples.keys())):
  if samples.has_key(i): 
    samplist.append(samples[i])
    notelist.append(notes[i])
    efflist.append(effects[i])
  else: 
   samplist.append([])
   notelist.append([])
   efflist.append([])
 return samplist,notelist,efflist,speed

def mod_lists(fn,fps=25.0):
 data=open(fn).read()
 for i in range(20): 
  if data[i]==0: break
 song_name=data[:i]
 song_length=ord(data[950])
 patterns=map(lambda x: ord(data[952+x]),range(song_length))
 num_pat=max(patterns)+1
 ident=data[1080:1084]
 assert ident=='M.K.'
 print 'Song Name = "%s", Length = %d, Patterns = %d' % (song_name,song_length,num_pat)
 sample_list,note_list,effect_list=[],[],[]
 speed=6
 for i,p in enumerate(patterns):
  s,n,e,speed=get_pattern(data,p,speed,fps)
  sample_list+=s
  note_list+=n
  effect_list+=e
 len_tot=float(len(sample_list))/fps
 len_min=len_tot/60
 len_s=len_tot % 60
 print 'Speed = %d, length = %d:%02d (%d frames)' % (speed,len_min,len_s,len(sample_list))
 return sample_list,note_list,effect_list

def load_sprite(x):
 sprite=Image.open(spritedir+x)
 sz=int(sprite.size[0]/sprite_scaling),int(sprite.size[1]/sprite_scaling)
 return sprite.resize(sz)

def rnd_pos(x):
 x=random.random()*(mask.size[0]-mask.size[0]/2)+mask.size[0]/4
 y=random.random()*(mask.size[1]-mask.size[0]/2)+mask.size[0]/4
 while mask.getpixel((int(x),int(y)))[0]==255: x,y=random.random()*mask.size[0],random.random()*mask.size[1]
 return [x,y]

flat=lambda z : reduce(lambda x,y:list(x)+list(y),z,[])

def limit(x,m):
 if x>=m: return m-1
 if x<0: return 0
 return x

def bright(iimg,n,cidx):
 oimg=Image.new('RGBA',iimg.size)
 for x in range(iimg.size[0]):
  for y in range(iimg.size[1]):
   c=iimg.getpixel((x,y))
   cl=list(c)
   if cl[3]>0: 
#    cl[3]=int(n*255/(fade_nb-1))
    if cidx>-1: cl[cidx]=int(cl[cidx]+(200-cl[cidx])/fade_nb*(fade_nb-n))
   oimg.putpixel((x,y),tuple(cl))
 return oimg

def exe(c):
 print c
 os.system(c)

fn=sys.argv[1]
# empty directory for frames
odir='/tmp/' + os.path.basename(fn).split('.')[0]
if os.path.isdir(odir): empty_directory(odir)
else: os.mkdir(odir)
# load and analyze MOD file
sample_list,note_list,effect_list=mod_lists(fn)
used_samples=filter(int,sorted(list(set(flat(sample_list)))))
used_notes=sorted(list(set(flat(note_list))))
used_effects=sorted(list(set(flat(effect_list))))
if debug:
 print 'Samples = %d, Notes = %d' % (len(used_samples),len(used_notes))
 print 'Used effects:',', '.join(map(lambda x : '"%s"' % commands[x],filter(int,used_effects)))
sampnotes={}
for u in used_samples: 
 sampnotes[u]=[]
 for s,n in zip(flat(sample_list),flat(note_list)):
  if s==u and n not in sampnotes[u]: sampnotes[u].append(n)

# load background images and mask
import Image,random,math,ImageDraw
bgfiles=sorted(os.listdir(bgdir))
assert len(bgfiles)>=len(sample_list)
mask=Image.open('/opt/img/kg_mask.png')
spritefiles=sorted(os.listdir(spritedir))
random.shuffle(spritefiles)
channels=4
sprites=map(load_sprite,spritefiles[:channels])
spritei=channels
# set initial positions, angle, speed, turning-state and blinking-state
mx,my=mask.size[0]/2,mask.size[1]/2
positions=[[mx-50,my-50],[mx-50,my+50],[mx+50,my-50],[mx+50,my+50]]
degrees=[225,315,135,45]
speeds=[0.0]*len(sprites)
turns=[0]*len(sprites)
blinks=[0]*len(sprites)

# frame creation loop
import progressbar
pbar = progressbar.ProgressBar().start()
for i,(s,n,e) in enumerate(zip(sample_list,note_list,effect_list)):
 im=Image.open(bgdir+bgfiles[i])
 if debug: draw=ImageDraw.Draw(im)
 for chan,(si,ni,ei) in enumerate(zip(s,n,e)):
  if si: np=float(sampnotes[si].index(ni))/float(len(sampnotes[si]))
  else: np=0.1
  speeds[chan]=np*10
  if ei and 'Vol' not in commands[ei]: blinks[chan]=fade_nb*2
  if np>0.8 and not turns[chan]: degrees[chan]+=1
  if np<0.2 and not turns[chan]: degrees[chan]-=1
 for n,spr in enumerate(sprites):
  positions[n][0]+=math.sin(math.radians(degrees[n]))*speeds[n]
  positions[n][1]+=math.cos(math.radians(degrees[n]))*speeds[n]
  p=limit(int(positions[n][0]),mask.size[0]),limit(int(positions[n][1]),mask.size[1])
  if (p[0]<spr.size[0] or p[0]>mask.size[0]-spr.size[0]) and not turns[n]: 
   degrees[n]=360-degrees[n]
   turns[n]=waiting_turns
  if (p[1]<spr.size[1] or p[1]>mask.size[1]-spr.size[1]) and not turns[n]: 
   degrees[n]=180-degrees[n]
   turns[n]=waiting_turns
   if p[1]>mask.size[1]-spr.size[1]:
    sprites[n]=load_sprite(spritefiles[spritei])
    spritei+=1
    if spritei>=len(spritefiles): spritei=0
  if mask.getpixel(p)[0]==255 and not turns[n] and speeds[n]>0:
   degrees[n]+=180
   turns[n]=waiting_turns
  if turns[n]: 
   if degrees[n]>360: degrees[n]=degrees[n]-360
   if degrees[n]<0: degrees[n]=degrees[n]+360
   turns[n]-=1
  if blinks[n]:
   if blinks[n]>fade_nb: fade_pos=blinks[n]-fade_nb
   else: fade_pos=fade_nb-blinks[n]
   spr=bright(spr,fade_pos,2)
   blinks[n]-=1
  im.paste(spr,p,spr)
 if debug: draw.text(p,'%d' % degrees[n],fill='red')
 im.save('%s/image%05d.jpg' % (odir,i))
 pbar.update(int(float(i)/float(len(sample_list))*100))
pbar.finish()

# create movie from frames and add converted MOD music
afn='/tmp/'+fn.lower().replace('mod','avi')
afn2=fn.lower().replace('mod','avi')
wfn='/tmp/'+fn.lower().replace('mod','wav')
mfn='/tmp/'+fn.lower().replace('mod','mp3')
assert fn!=afn2
exe('mencoder mf://%s/*.jpg -ovc x264 -x264encopts crf=20 -o %s -mf fps=25' % (odir,afn) )
if not os.path.isfile(wfn): exe('timidity %s -Ow -o %s' % (fn,wfn))
if not os.path.isfile(mfn): exe('lame %s %s' % (wfn,mfn))
if os.path.isfile(afn2): os.unlink(afn2)
exe('ffmpeg -i %s -i %s -vcodec copy %s -acodec copy -newaudio' % (afn,mfn,afn2))