#! /ufs/guido/bin/sgi/python

# 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 to events
# while the sample is playing, so you can change the volume (gain)
# during playing, cancel playing or start a new sample right away.
#
# The control window displays the current output gain and a primitive
# "stop button" to cancel the current play request.
#
# Sound files must currently be in Dik Winter's compressed Mac format.
# Since decompression is costly, decompressed samples are saved in
# /usr/tmp/@j* until the application is left.  The files are read
# afresh each time, though.

import audio
import sunaudio
import commands
import getopt
import path
import posix
import rand
import stdwin
from stdwinevents import *
import string
import sys

from WindowParent import WindowParent
from HVSplit import VSplit
from Buttons import PushButton
from Sliders import ComplexSlider

# Pathnames

HOME_BIN_SGI = '/ufs/guido/bin/sgi/'   # Directory where macsound2sgi lives
DEF_DB = '/ufs/dik/sounds/Mac/HCOM'    # Default directory of sounds


# 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.gain = 75             # Output gain
       G.rate = 3              # Sampling rate
       G.busy = 0              # Set while asynchronous playing is active
       G.windows = []          # List of open windows (except control)
       G.mode = 'mac'          # Macintosh mode
       G.tempprefix = '/usr/tmp/@j' + `rand.rand()` + '-'
       #
       optlist, args = getopt.getopt(sys.argv[1:], 'dg:r:sSa')
       for optname, optarg in optlist:
               if   optname = '-d':
                       G.debug = 1
               elif optname = '-g':
                       G.gain = string.atoi(optarg)
                       if not (0 < G.gain < 256):
                               raise optarg.error, '-g gain out of range'
               elif optname = '-r':
                       G.rate = string.atoi(optarg)
                       if not (1 <= G.rate <= 3):
                               raise optarg.error, '-r rate out of range'
               elif optname = '-s':
                       G.synchronous = 1
               elif optname = '-S':
                       G.mode = 'sgi'
               elif optname = '-a':
                       G.mode = 'sun'
       #
       if not args:
               args = [DEF_DB]
       #
       G.cw = opencontrolwindow()
       for dirname in args:
               G.windows.append(openlistwindow(dirname))
       #
       #
       savegain = audio.getoutgain()
       try:
               # Initialize stdaudio
               audio.setoutgain(0)
               audio.start_playing('')
               dummy = audio.wait_playing()
               audio.setoutgain(0)
               maineventloop()
       finally:
               audio.setoutgain(savegain)
               audio.done()
               clearcache()

def maineventloop():
       mouse_events = WE_MOUSE_DOWN, WE_MOUSE_MOVE, WE_MOUSE_UP
       while G.windows:
               type, w, detail = event = stdwin.getevent()
               if w = G.cw.win:
                       if type = WE_CLOSE:
                               return
                       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: print type, w, detail

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

def opencontrolwindow():
       cw = WindowParent().create('Jukebox', (0, 0))
       v = VSplit().create(cw)
       #
       gain = ComplexSlider().define(v)
       gain.setminvalmax(0, G.gain, 255)
       gain.settexts('  ', '  ')
       gain.sethook(gain_setval_hook)
       #
       stop = PushButton().definetext(v, 'Stop')
       stop.hook = stop_hook
       #
       cw.realize()
       return cw

def gain_setval_hook(self):
       G.gain = self.val
       if G.busy: audio.setoutgain(G.gain)

def stop_hook(self):
       if G.busy:
               audio.setoutgain(0)
               dummy = audio.stop_playing()
               G.busy = 0


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

def openlistwindow(dirname):
       list = posix.listdir(dirname)
       list.sort()
       i = 0
       while i < len(list):
               if list[i] = '.' or list[i] = '..':
                       del list[i]
               else:
                       i = i+1
       for i in range(len(list)):
               name = list[i]
               if path.isdir(path.cat(dirname, name)):
                       list[i] = list[i] + '/'
       width = maxwidth(list)
       width = width + stdwin.textwidth(' ')   # XXX X11 stdwin bug workaround
       height = len(list) * stdwin.lineheight()
       stdwin.setdefwinsize(width, min(height, 500))
       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 in list:
               w = stdwin.textwidth(name)
               if w > width: width = w
       return width

def drawlistwindow(w, area):
       d = w.begindrawing()
       d.erase((0, 0), (1000, 10000))
       lh = d.lineheight()
       h, v = 0, 0
       for name in w.list:
               d.text((h, v), name)
               v = v + lh
       showselection(w, d)

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)
       if type = WE_MOUSE_DOWN and clicks >= 2 and i >= 0:
               name = path.cat(w.dirname, w.list[i])
               if name[-1:] = '/':
                       if clicks = 2:
                               G.windows.append(openlistwindow(name[:-1]))
               else:
                       playfile(name)

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

def remove(list, item):
       for i in range(len(list)):
               if list[i] = item:
                       del list[i]
                       break


# Playing tools

cache = {}

def clearcache():
       for x in cache.keys():
               try:
                       sts = posix.system('rm -f ' + cache[x])
                       if sts:
                               print cmd
                               print 'Exit status', sts
               except:
                       print cmd
                       print 'Exception?!'
               del cache[x]

def playfile(name):
       if G.mode <> 'mac':
               tempname = name
       elif cache.has_key(name):
               tempname = cache[name]
       else:
               tempname = G.tempprefix + `rand.rand()`
               cmd = HOME_BIN_SGI + 'macsound2sgi'
               cmd = cmd + ' ' + commands.mkarg(name)
               cmd = cmd + ' >' + tempname
               if G.debug: print cmd
               sts = posix.system(cmd)
               if sts:
                       print cmd
                       print 'Exit status', sts
                       stdwin.fleep()
                       return
               cache[name] = tempname
       fp = open(tempname, 'r')
       try:
               hdr = sunaudio.gethdr(fp)
       except sunaudio.error, msg:
               hdr = ()
       if hdr:
               data_size = hdr[0]
               data = fp.read(data_size)
               # XXX this doesn't work yet, need to convert from uLAW!!!
               del fp
       else:
               del fp
               data = readfile(tempname)
       if G.debug: print len(data), 'bytes read from', tempname
       if G.busy:
               G.busy = 0
               dummy = audio.stop_playing()
       #
       # Completely reset the audio device
       audio.setrate(G.rate)
       audio.setduration(0)
       audio.setoutgain(G.gain)
       #
       if G.synchronous:
               audio.write(data)
               audio.setoutgain(0)
       else:
               try:
                       audio.start_playing(data)
                       G.busy = 1
               except:
                       stdwin.fleep()
       del data

def readfile(filename):
       return readfp(open(filename, 'r'))

def readfp(fp):
       data = ''
       while 1:
               buf = fp.read(102400) # Reads most samples in one fell swoop
               if not buf:
                       return data
               data = data + buf

main()
