#! /usr/bin/env python

# XXX This only works on SGIs running IRIX 4.0 or higher

# JUKEBOX: browse directories full of sampled sound files.
#
# One or more "list windows" display the files and subdirectories of
# the arguments.  Double-clicking on a subdirectory opens a new window
# displaying its contents (and so on recursively).  Double clicking
# on a file plays it as a sound file (assuming it is one).
#
# Playing is asynchronous: the application keeps listening for events
# while the sample is playing, so you can cancel playing or start a
# new sample right away.  Synchronous playing is available through the
# -s option.
#
# The control window displays a "stop button" that cancel the current
# play request.
#
# Most sound file formats recognized by SOX or SFPLAY are recognized.
# Since conversion is costly, converted files are cached in
# /usr/tmp/@j* until the user quits or changes the sampling rate via
# the Rate menu.

import commands
import getopt
import os
from stat import *
import rand
import stdwin
from stdwinevents import *
import sys
import tempfile
import sndhdr

from WindowParent import WindowParent
from Buttons import PushButton

# Pathnames

DEF_DB = '/usr/local/sounds'		# Default directory of sounds
SOX = '/usr/local/bin/sox'		# Sound format conversion program
SFPLAY = '/usr/sbin/sfplay'		# Sound playing program


# Global variables

class struct: pass		# Class to define featureless structures

G = struct()			# Holds writable global variables


# Main program

def main():
	G.synchronous = 0	# If set, use synchronous audio.write()
	G.debug = 0		# If set, print debug messages
	G.busy = 0		# Set while asynchronous playing is active
	G.windows = []		# List of open windows, except control
	G.mode = ''		# File type (default any that sfplay knows)
	G.rate = 0		# Sampling rate (default " " " ")
	G.tempprefix = tempfile.mktemp()
	#
	try:
		optlist, args = getopt.getopt(sys.argv[1:], 'dr:st:')
	except getopt.error, msg:
		sys.stdout = sys.stderr
		print msg
		print 'usage: jukebox [-d] [-s] [-t type] [-r rate]'
		print '  -d        debugging (-dd event debugging)'
		print '  -s        synchronous playing'
		print '  -t type   file type'
		print '  -r rate   sampling rate'
		sys.exit(2)
	#
	for optname, optarg in optlist:
		if   optname == '-d':
			G.debug = G.debug + 1
		elif optname == '-r':
			G.rate = int(eval(optarg))
		elif optname == '-s':
			G.synchronous = 1
		elif optname == '-t':
			G.mode = optarg
	#
	if G.debug:
		for name in G.__dict__.keys():
			print 'G.' + name, '=', `G.__dict__[name]`
	#
	if not args:
		args = [DEF_DB]
	#
	G.cw = opencontrolwindow()
	for dirname in args:
		G.windows.append(openlistwindow(dirname))
	#
	#
	try:
		maineventloop()
	finally:
		clearcache()
		killchild()

# Entries in Rate menu:
rates = ['default', '7350', \
	'8000', '11025', '16000', '22050', '32000', '41000', '48000']

def maineventloop():
	mouse_events = WE_MOUSE_DOWN, WE_MOUSE_MOVE, WE_MOUSE_UP
	while G.windows:
		try:
			type, w, detail = event = stdwin.getevent()
		except KeyboardInterrupt:
			killchild()
			continue
		if w == G.cw.win:
			if type == WE_CLOSE:
				return
			if type == WE_TIMER:
				checkchild()
				if G.busy:
					G.cw.win.settimer(1)
			elif type == WE_MENU:
				menu, item = detail
				if menu is G.ratemenu:
					clearcache()
					if item == 0:
						G.rate = 0
					else:
						G.rate = eval(rates[item])
					for i in range(len(rates)):
						menu.check(i, (i == item))
			else:
				G.cw.dispatch(event)
		else:
			if type == WE_DRAW:
				w.drawproc(w, detail)
			elif type in mouse_events:
				w.mouse(w, type, detail)
			elif type == WE_CLOSE:
				w.close(w)
				del w, event
			else:
				if G.debug > 1: print type, w, detail

def checkchild():
	if G.busy:
		waitchild(1)

def killchild():
	if G.busy:
		os.kill(G.busy, 9)
		waitchild(0)

def waitchild(options):
	pid, sts = os.waitpid(G.busy, options)
	if pid == G.busy:
		G.busy = 0
		G.stop.enable(0)


# Control window -- to set gain and cancel play operations in progress

def opencontrolwindow():
	stdwin.setdefscrollbars(0, 0)
	cw = WindowParent().create('Jukebox', (0, 0))
	#
	stop = PushButton().definetext(cw, '        Stop        ')
	stop.hook = stop_hook
	stop.enable(0)
	G.stop = stop
	#
	cw.realize()
	#
	G.ratemenu = cw.win.menucreate('Rate')
	for r in rates:
		G.ratemenu.additem(r)
	if G.rate == 0:
		G.ratemenu.check(0, 1)
	else:
		for i in len(range(rates)):
			if rates[i] == `G.rate`:
				G.ratemenu.check(i, 1)
	#
	return cw

def stop_hook(self):
	killchild()


# List windows -- to display list of files and subdirectories

def openlistwindow(dirname):
	list = os.listdir(dirname)
	list.sort()
	i = 0
	while i < len(list):
		if list[i][0] == '.':
			del list[i]
		else:
			i = i+1
	for i in range(len(list)):
		fullname = os.path.join(dirname, list[i])
		if os.path.isdir(fullname):
			info = '/'
		else:
			try:
				size = os.stat(fullname)[ST_SIZE]
				info = `(size + 1023)/1024` + 'k'
			except IOError:
				info = '???'
			info = '(' + info + ')'
		list[i] = list[i], info
	width = maxwidth(list)
	# width = width + stdwin.textwidth(' ')	# XXX X11 stdwin bug workaround
	height = len(list) * stdwin.lineheight()
	stdwin.setdefwinsize(width, min(height, 500))
	stdwin.setdefscrollbars(0, 1)
	w = stdwin.open(dirname)
	stdwin.setdefwinsize(0, 0)
	w.setdocsize(width, height)
	w.drawproc = drawlistwindow
	w.mouse = mouselistwindow
	w.close = closelistwindow
	w.dirname = dirname
	w.list = list
	w.selected = -1
	return w

def maxwidth(list):
	width = 1
	for name, info in list:
		w = stdwin.textwidth(name + '  ' + info)
		if w > width: width = w
	return width

def drawlistwindow(w, area):
##	(left, top), (right, bottom) = area
	d = w.begindrawing()
	d.erase((0, 0), (1000, 10000))
	lh = d.lineheight()
	h, v = 0, 0
	for name, info in w.list:
		if info == '/':
			text = name + '/'
		else:
			text = name + '  ' + info
		d.text((h, v), text)
		v = v + lh
	showselection(w, d)
	d.close()

def hideselection(w, d):
	if w.selected >= 0:
		invertselection(w, d)

def showselection(w, d):
	if w.selected >= 0:
		invertselection(w, d)

def invertselection(w, d):
	lh = d.lineheight()
	h1, v1 = p1 = 0, w.selected*lh
	h2, v2 = p2 = 1000, v1 + lh
	d.invert(p1, p2)

def mouselistwindow(w, type, detail):
	(h, v), clicks, button = detail[:3]
	d = w.begindrawing()
	lh = d.lineheight()
	if 0 <= v < lh*len(w.list):
		i = v / lh
	else:
		i = -1
	if w.selected <> i:
		hideselection(w, d)
		w.selected = i
		showselection(w, d)
	d.close()
	if type == WE_MOUSE_DOWN and clicks >= 2 and i >= 0:
		setcursors('watch')
		name, info = w.list[i]
		fullname = os.path.join(w.dirname, name)
		if info == '/':
			if clicks == 2:
				G.windows.append(openlistwindow(fullname))
		else:
			playfile(fullname)
		setcursors('cross')

def closelistwindow(w):
	G.windows.remove(w)

def setcursors(cursor):
	for w in G.windows:
		w.setwincursor(cursor)
	G.cw.win.setwincursor(cursor)


# Playing tools

cache = {}

def clearcache():
	for x in cache.keys():
		cmd = 'rm -f ' + cache[x]
		if G.debug: print cmd
		sts = os.system(cmd)
		if sts:
			print cmd
			print 'Exit status', sts
		del cache[x]

validrates = (8000, 11025, 16000, 22050, 32000, 44100, 48000)

def playfile(filename):
	killchild()
	try:
		tuple = sndhdr.what(filename)
	except IOError, msg:
		print 'Can\'t open', filename, msg
		stdwin.fleep()
		return
	raw = 0
	if tuple:
		mode, rate = tuple[:2]
		if rate == 0:
			rate = G.rate
			if rate == 0:
				rate = 8000
	else:
		mode = G.mode
		rate = G.rate
	if G.debug: print 'mode =', mode, 'rate =', rate
	if mode in ('au', 'aiff', 'wav', 'aifc', 'ul', 'ub', 'sb') and \
		  rate in validrates:
		tempname = filename
		if mode in ('ul', 'ub', 'sb'):
			raw = 1
	elif cache.has_key(filename):
		tempname = cache[filename]
	else:
		tempname = G.tempprefix + `rand.rand()` + '.aiff'
		cmd = SOX
		if G.debug:
			cmd = cmd + ' -V'
		if mode <> '':
			cmd = cmd + ' -t ' + mode
		cmd = cmd + ' ' + commands.mkarg(filename)
		cmd = cmd + ' -t aiff'
		if rate not in validrates:
			rate = 32000
		if rate:
			cmd = cmd + ' -r ' + `rate`
		cmd = cmd + ' ' + tempname
		if G.debug: print cmd
		sts = os.system(cmd)
		if sts:
			print cmd
			print 'Exit status', sts
			stdwin.fleep()
			try:
				os.unlink(tempname)
			except:
				pass
			return
		cache[filename] = tempname
	if raw:
		pid = sfplayraw(tempname, tuple)
	else:
		pid = sfplay(tempname, [])
	if G.synchronous:
		sts = os.wait(pid, 0)
	else:
		G.busy = pid
		G.stop.enable(1)
		G.cw.win.settimer(1)

def sfplayraw(filename, tuple):
	args = ['-i']
	type, rate, channels, frames, bits = tuple
	if type == 'ul':
		args.append('mulaw')
	elif type == 'ub':
		args = args + ['integer', '8', 'unsigned']
	elif type == 'sb':
		args = args + ['integer', '8', '2scomp']
	else:
		print 'sfplayraw: warning: unknown type in', tuple
	if channels > 1:
		args = args + ['channels', `channels`]
	if not rate:
		rate = G.rate
	if rate:
		args = args + ['rate', `rate`]
	args.append('end')
	return sfplay(filename, args)

def sfplay(filename, args):
	if G.debug:
		args = ['-p'] + args
	args = [SFPLAY, '-r'] + args + [filename]
	if G.debug: print 'sfplay:', args
	pid = os.fork()
	if pid == 0:
		# Child
		os.execv(SFPLAY, args)
		# NOTREACHED
	else:
		# Parent
		return pid

main()
