| #! /usr/bin/env 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.join(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.join(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() |