#!/usr/bin/env python
"""Jack Module Player v0.1b"""

author='Janne Halttunen'
author_email='jhalttun@pp.htv.fi'

requires="""Extended Module Player: http://xmp.sourceforge.net/
Ecasound: http://eca.cx/
"""

import os, sys, signal, glob
from popen2 import Popen4

def get_channels(mfile, verbose=0):
    xmpin,xmp=os.popen4('xmp %s --load-only -c' % mfile, 'r', 0)
    lines=xmp.readlines()
    xmp.close()
    xmpin.close()
    for l in lines:
	if verbose:
	    print l.strip()
	
	if l[:8]=='Channels':
	    splist=filter(None, l.split(' '))
	    return int(splist[2])
		
def get_jack_ports():
    jin,jack=os.popen4('jack_lsp', 'r', 0)
    lines=jack.readlines()
    jack.close()
    jin.close()
    
    ports=[]
    for l in lines:
	ports.append(l.strip())
    return ports
	
usage="""
usage: jmp.py [options] module-file(s)"""

help="""%s

jmp.py options:
    
    -b, --buffer     set buffersize 
    -r, --rate       set sample rate
    -c, --channels   override module's number of channels
    -N, --names      name individual channels (or channels sticked)
                     with comma separated list of names
		     e.g. bassdrum,hihat,snare
		     
    -S, --stick      stick individual channels together
                     with colon separated list of channel-numbers
                     e.g. 0-3,5:6,7:8  (channel-numbering starts at 0)
		     
    -l, --looping    loop module
    -n, --noconnect  don't connect jack-ports to alsa-playback
    -q, --quiet
    -v, --verbose
""" % usage

def main():    
    args={}
    next=''
    
    args['rate']='48000'
    args['buffer']='1024'
    args['looping']=''
    args['file']=[]
        
    for a in sys.argv[1:]:
	if a in ('-b','--buffer'):
	    next='buffer'
	elif a in ('-r', '--rate'):
	    next='rate'
	elif a in ('-c', '--channels'):
	    next='channels'
	elif a in ('-N', '--names'):
	    next='names'
	elif a in ('-S', '--stick'):
	    next='sticks'
	elif a in ('-n', '--noconnect'):
	    args['noconnect']=1
	elif a in ('-l', '--looping'):
	    args['looping']='-l'
	elif a in ('-q', '--quiet'):
	    args['quiet']=1
	elif a in ('-v', '--verbose'):
	    args['verbose']=1
	elif next:
	    args[next]=a
	elif a in ('-h','--help'):
	    print __doc__
	    print help
	    sys.exit()
	else:
	    args['file']+=glob.glob(a)
	    
    if not args.get('quiet'):
	print __doc__
	
    if not args.get('file'):
	print usage
	sys.exit()
	
	        
    if args.get('channels'):
	ch=int(args['channels'])
    else:
	ch=0
	for f in args['file']:
	    c=get_channels(f, args.get('verbose'))
	    if c>ch:
		ch=c
	
    if args.get('names'):
	args['name']=args['names'].split(',')	
    else:
	args['name']=[]

    args['name']+= ['xmp']*(ch-len(args['name']))
    
    if args.get('sticks'):
	args['stick']=args['sticks'].split(':')
    else:
	args['stick']=[]
	
    if args['stick']:
	if not args['stick'][-1]:
	    args['stick']=args['stick'][:-1]
	    for i in range(int(args['stick'][-1][-1]), ch-1):
		args['stick'].append(str(i+1))
    else:
	for i in range(ch):
	    args['stick'].append(str(i))
	    
    ch=len(args['stick'])
	        
    if ch:
	print 'channels: %d' % ch
	if not args.get('looping'):
	    print 'not looping..'

    	for i in range(ch):
	    if not os.path.exists('/tmp/xmp%d.raw' % (i+1)):
		os.mkfifo('/tmp/xmp%d.raw' % (i+1))
    
	xmp=[]
    	for i in range(ch):
	    xmp.append(Popen4('xmp %s -m %s -f%s -q --solo %s -o /tmp/xmp%d.raw &'  % (' '.join(args['file']), args['looping'], args['rate'], args['stick'][i], (i+1)), 0 ))
#	    os.popen('xmp %s -m %s -f%s -q --solo %s -o /tmp/xmp%d.raw 2>&1 &'  % (args['file'], args['looping'], args['rate'], args['stick'][i], (i+1)), 'r' )
	    
    	eline='ecasound -b %s -f s16_le,1,%s ' % (args['buffer'], args['rate'])
    	for i in range(ch):
	    eline+='-a x%d -i /tmp/xmp%d.raw -o jack_generic,%s ' % (i+1, i+1, args['name'][i])
    
	eline+='2>&1 -c'
    
    	eca=Popen4(eline, 0)
	eca.tochild.write('cs-connect\n')
	    	
    	while 1:
	    try: 
		line=eca.fromchild.readline().strip()
		if not args.get('quiet') \
		and args.get('verbose') \
		and line.strip():
		    print line
		if line.count('Connected chainsetup'):
		    		    
		    eca.tochild.write('start\n')
		    
		    if not args.get('noconnect'):			
			
			ports=get_jack_ports()
			
			if not args.get('quiet'):
			    print 'connecting ports'
			    
			for p in ports:
			    
			    if p.count('ecasound') \
			    and filter(p.count, args['name']):
			    
				if not args.get('quiet'):
				    print 'connecting %s to playback' % p
				os.popen('jack_connect %s alsa_pcm:playback_1' % p, 'r')
				os.popen('jack_connect %s alsa_pcm:playback_2' % p, 'r')
			    
			if not args.get('quiet'):
			    print 'done.'
			    			    
			args['noconnect']=1
		elif line.count('DBC_REQUIRE failed') \
		or line.count('engine has shut down') \
		or line.count('engine unexpectedly shutdown'):
		    if not args.get('quiet'):
			print 'exiting..'
		    os.kill(eca.pid, signal.SIGTERM)
		    os.popen4('killall xmp')
		    break
		elif line.count('Unable to open JACK-client'):
		    if not args.get('quiet'):
			print 'cannot connect jack, exiting..'
		    os.kill(eca.pid, signal.SIGTERM)
		    os.popen4('killall xmp')
		    break
	    except:
		if not args.get('quiet'):
		    print 'break'
		os.kill(eca.pid, signal.SIGTERM)
		os.popen4('killall xmp')
		raise
    else: raise RuntimeError, 'No channels acquired!'

if __name__=='__main__':
    try: main()
    except KeyboardInterrupt: pass
    except:
	print '\nNOTE! external programs required:\n'
	print requires
	raise
