#!/usr/bin/python
# MOD file synchronizer, 2008-11-24 by Nick Fankhauser
# Synchronizes the speed of timelapse to a MOD channel
# Requires: mencoder, ffmpeg, lame, timidity

import sys,os,shutil,numpy,progressbar
abspeed=0.02
bgdir='/home/nyk/fasnacht/'

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

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

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

fn=sys.argv[1]
used_channel=int(sys.argv[2]) # channel to use [0-3]
# empty directory for frames
odir='/tmp/' + str(used_channel) + 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))))
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)

bgfiles=sorted(os.listdir(bgdir))
print 'Images:',len(bgfiles)
inps=[]
for i,(s,n,e) in enumerate(zip(sample_list,note_list,effect_list)):
 npl=[]
 for chan,(si,ni,ei) in enumerate(zip(s,n,e)):
  if si: npl.append(float(sampnotes[si].index(ni))/float(len(sampnotes[si])))
  else: npl.append(0)
 if len(npl): mean=npl[used_channel]
 else: mean=0
 inps.append(mean)
avg_inp=float(sum(inps))/float(len(inps))
ffakt=float(len(inps))/float(len(bgfiles))
print 'Average intensity = %2.2f, Frame factor = %2.2f' % (avg_inp,ffakt)

pos=0.0
pbar = progressbar.ProgressBar().start()
# frame creation loop
for n,i in enumerate(inps):
 pos+=(i/avg_inp)/ffakt
 if pos>=len(bgfiles): pos=len(bgfiles)-1
 shutil.copy(bgdir+bgfiles[int(pos)],'%s/image%05d.jpg' % (odir,n))
 percent_complete=int(float(n)/float(len(inps))*100)
 pbar.update(percent_complete)
pbar.finish()

# create movie from frames and add converted MOD music
afn='/tmp/'+str(used_channel)+fn.lower().replace('mod','avi')
afn2=str(used_channel)+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))