The 0.5 release happened on 2/15, not on 2/14. :-)
[python/dscho.git] / Tools / audiopy / audiopy
blobf9193dd854aad63f39f5dfc9cdbdbb4ea9c3d7cd
1 #! /usr/bin/env python
3 """audiopy -- a program to control the Solaris audio device.
5 Contact: Barry Warsaw
6 Email: bwarsaw@python.org
7 Version: %(__version__)s
9 When no arguments are given, this pops up a graphical window which lets you
10 choose the audio input and output devices, and set the output volume.
12 This program can be driven via the command line, and when done so, no window
13 pops up. Most options have the general form:
15 --device[={0,1}]
16 -d[={0,1}]
17 Set the I/O device. With no value, it toggles the specified device.
18 With a value, 0 turns the device off and 1 turns the device on.
20 The list of devices and their short options are:
22 (input)
23 microphone -- m
24 linein -- i
25 cd -- c
27 (output)
28 headphones -- p
29 speaker -- s
30 lineout -- o
32 Other options are:
34 --gain volume
35 -g volume
36 Sets the output gain to the specified volume, which must be an integer
37 in the range [%(MIN_GAIN)s..%(MAX_GAIN)s]
39 --version
41 Print the version number and exit.
43 --help
45 Print this message and exit.
46 """
48 import sys
49 import os
50 import string
51 import errno
52 import sunaudiodev
53 from SUNAUDIODEV import *
55 # Milliseconds between interrupt checks
56 KEEPALIVE_TIMER = 500
58 __version__ = '1.1'
62 class MainWindow:
63 def __init__(self, device):
64 from Tkinter import *
65 self.__helpwin = None
66 self.__devctl = device
67 info = device.getinfo()
69 self.__tkroot = tkroot = Tk(className='Audiopy')
70 tkroot.withdraw()
71 # create the menubar
72 menubar = Menu(tkroot)
73 filemenu = Menu(menubar, tearoff=0)
74 filemenu.add_command(label='Quit',
75 command=self.__quit,
76 accelerator='Alt-Q',
77 underline=0)
78 helpmenu = Menu(menubar, name='help', tearoff=0)
79 helpmenu.add_command(label='About Audiopy...',
80 command=self.__popup_about,
81 underline=0)
82 helpmenu.add_command(label='Help...',
83 command=self.__popup_using,
84 underline=0)
85 menubar.add_cascade(label='File',
86 menu=filemenu,
87 underline=0)
88 menubar.add_cascade(label='Help',
89 menu=helpmenu,
90 underline=0)
91 # now create the top level window
92 root = self.__root = Toplevel(tkroot, class_='Audiopy', menu=menubar)
93 root.protocol('WM_DELETE_WINDOW', self.__quit)
94 root.title('audiopy ' + __version__)
95 root.iconname('audiopy ' + __version__)
96 root.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive)
98 buttons = []
100 # where does input come from?
101 frame = Frame(root, bd=1, relief=RAISED)
102 frame.grid(row=1, column=0, sticky='NSEW')
103 label = Label(frame, text='Input From:')
104 label.grid(row=0, column=0, sticky=E)
105 self.__inputvar = IntVar()
107 btn = Radiobutton(frame,
108 text='None',
109 variable=self.__inputvar,
110 value=0,
111 command=self.__pushtodev,
112 underline=0)
113 btn.grid(row=0, column=1, sticky=W)
114 root.bind('<Alt-n>', self.__none)
115 root.bind('<Alt-N>', self.__none)
116 if not info.i_avail_ports & MICROPHONE:
117 btn.configure(state=DISABLED)
118 buttons.append(btn)
120 btn = Radiobutton(frame,
121 text='Microphone',
122 variable=self.__inputvar,
123 value=MICROPHONE,
124 command=self.__pushtodev,
125 underline=0)
126 btn.grid(row=1, column=1, sticky=W)
127 root.bind('<Alt-m>', self.__mic)
128 root.bind('<Alt-M>', self.__mic)
129 if not info.i_avail_ports & MICROPHONE:
130 btn.configure(state=DISABLED)
131 buttons.append(btn)
133 btn = Radiobutton(frame,
134 text='Line In',
135 variable=self.__inputvar,
136 value=LINE_IN,
137 command=self.__pushtodev,
138 underline=5)
139 btn.grid(row=2, column=1, sticky=W)
140 root.bind('<Alt-i>', self.__linein)
141 root.bind('<Alt-I>', self.__linein)
142 if not info.i_avail_ports & LINE_IN:
143 btn.configure(state=DISABLED)
144 buttons.append(btn)
145 ## if SUNAUDIODEV was built on an older version of Solaris, the CD
146 ## input device won't exist
147 try:
148 btn = Radiobutton(frame,
149 text='CD',
150 variable=self.__inputvar,
151 value=CD,
152 command=self.__pushtodev,
153 underline=0)
154 btn.grid(row=3, column=1, sticky=W)
155 root.bind('<Alt-c>', self.__cd)
156 root.bind('<Alt-C>', self.__cd)
157 if not info.i_avail_ports & CD:
158 btn.configure(state=DISABLED)
159 buttons.append(btn)
160 except NameError:
161 pass
163 # where does output go to?
164 frame = Frame(root, bd=1, relief=RAISED)
165 frame.grid(row=2, column=0, sticky='NSEW')
166 label = Label(frame, text='Output To:')
167 label.grid(row=0, column=0, sticky=E)
168 self.__spkvar = IntVar()
169 btn = Checkbutton(frame,
170 text='Speaker',
171 variable=self.__spkvar,
172 onvalue=SPEAKER,
173 command=self.__pushtodev,
174 underline=0)
175 btn.grid(row=0, column=1, sticky=W)
176 root.bind('<Alt-s>', self.__speaker)
177 root.bind('<Alt-S>', self.__speaker)
178 if not info.o_avail_ports & SPEAKER:
179 btn.configure(state=DISABLED)
180 buttons.append(btn)
182 self.__headvar = IntVar()
183 btn = Checkbutton(frame,
184 text='Headphones',
185 variable=self.__headvar,
186 onvalue=HEADPHONE,
187 command=self.__pushtodev,
188 underline=4)
189 btn.grid(row=1, column=1, sticky=W)
190 root.bind('<Alt-p>', self.__headphones)
191 root.bind('<Alt-P>', self.__headphones)
192 if not info.o_avail_ports & HEADPHONE:
193 btn.configure(state=DISABLED)
194 buttons.append(btn)
196 self.__linevar = IntVar()
197 btn = Checkbutton(frame,
198 variable=self.__linevar,
199 onvalue=LINE_OUT,
200 text='Line Out',
201 command=self.__pushtodev,
202 underline=0)
203 btn.grid(row=2, column=1, sticky=W)
204 root.bind('<Alt-l>', self.__lineout)
205 root.bind('<Alt-L>', self.__lineout)
206 if not info.o_avail_ports & LINE_OUT:
207 btn.configure(state=DISABLED)
208 buttons.append(btn)
210 # Fix up widths
211 widest = 0
212 for b in buttons:
213 width = b['width']
214 if width > widest:
215 widest = width
216 for b in buttons:
217 b.configure(width=widest)
218 # root bindings
219 root.bind('<Alt-q>', self.__quit)
220 root.bind('<Alt-Q>', self.__quit)
222 # Volume
223 frame = Frame(root, bd=1, relief=RAISED)
224 frame.grid(row=3, column=0, sticky='NSEW')
225 label = Label(frame, text='Output Volume:')
226 label.grid(row=0, column=0, sticky=W)
227 self.__scalevar = IntVar()
228 self.__scale = Scale(frame,
229 orient=HORIZONTAL,
230 from_=MIN_GAIN,
231 to=MAX_GAIN,
232 length=200,
233 variable=self.__scalevar,
234 command=self.__volume)
235 self.__scale.grid(row=1, column=0, sticky=EW)
237 # do we need to poll for changes?
238 self.__needtopoll = 1
239 try:
240 fd = self.__devctl.fileno()
241 self.__needtopoll = 0
242 except AttributeError:
243 pass
244 else:
245 import fcntl
246 import signal
247 import STROPTS
248 # set up the signal handler
249 signal.signal(signal.SIGPOLL, self.__update)
250 fcntl.ioctl(fd, STROPTS.I_SETSIG, STROPTS.S_MSG)
251 self.__update()
253 def __quit(self, event=None):
254 self.__devctl.close()
255 self.__root.quit()
257 def __popup_about(self, event=None):
258 import tkMessageBox
259 tkMessageBox.showinfo('About Audiopy ' + __version__,
260 '''\
261 Audiopy %s
262 Control the Solaris audio device
264 For information
265 Contact: Barry A. Warsaw
266 Email: bwarsaw@python.org''' % __version__)
268 def __popup_using(self, event=None):
269 if not self.__helpwin:
270 self.__helpwin = Helpwin(self.__tkroot, self.__quit)
271 self.__helpwin.deiconify()
274 def __keepalive(self):
275 # Exercise the Python interpreter regularly so keyboard interrupts get
276 # through.
277 self.__tkroot.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive)
278 if self.__needtopoll:
279 self.__update()
281 def __update(self, num=None, frame=None):
282 # It's possible (although I have never seen it) to get an interrupted
283 # system call during the getinfo() call. If so, and we're polling,
284 # don't sweat it because we'll come around again later. Otherwise,
285 # we'll give it a couple of tries and then give up until next time.
286 tries = 0
287 while 1:
288 try:
289 info = self.__devctl.getinfo()
290 break
291 except sunaudiodev.error:
292 if self.__needtopoll or tries > 3:
293 return
294 tries = tries + 1
295 # input
296 self.__inputvar.set(info.i_port)
297 # output
298 self.__spkvar.set(info.o_port & SPEAKER)
299 self.__headvar.set(info.o_port & HEADPHONE)
300 self.__linevar.set(info.o_port & LINE_OUT)
301 # volume
302 self.__scalevar.set(info.o_gain)
304 def __pushtodev(self, event=None):
305 info = self.__devctl.getinfo()
306 info.o_port = self.__spkvar.get() + \
307 self.__headvar.get() + \
308 self.__linevar.get()
309 info.i_port = self.__inputvar.get()
310 info.o_gain = self.__scalevar.get()
311 self.__devctl.setinfo(info)
313 def __getset(self, var, onvalue):
314 if var.get() == onvalue:
315 var.set(0)
316 else:
317 var.set(onvalue)
318 self.__pushtodev()
320 def __none(self, event=None):
321 self.__inputvar.set(0)
322 self.__pushtodev()
324 def __mic(self, event=None):
325 self.__getset(self.__inputvar, MICROPHONE)
327 def __linein(self, event=None):
328 self.__getset(self.__inputvar, LINE_IN)
330 def __cd(self, event=None):
331 self.__getset(self.__inputvar, CD)
333 def __speaker(self, event=None):
334 self.__getset(self.__spkvar, SPEAKER)
336 def __headphones(self, event=None):
337 self.__getset(self.__headvar, HEADPHONE)
339 def __lineout(self, event=None):
340 self.__getset(self.__linevar, LINE_OUT)
342 def __volume(self, event=None):
343 self.__pushtodev()
345 def start(self):
346 self.__keepalive()
347 self.__tkroot.mainloop()
351 class Helpwin:
352 def __init__(self, master, quitfunc):
353 from Tkinter import *
354 self.__root = root = Toplevel(master, class_='Audiopy')
355 root.protocol('WM_DELETE_WINDOW', self.__withdraw)
356 root.title('Audiopy Help Window')
357 root.iconname('Audiopy Help Window')
358 root.bind('<Alt-q>', quitfunc)
359 root.bind('<Alt-Q>', quitfunc)
360 root.bind('<Alt-w>', self.__withdraw)
361 root.bind('<Alt-W>', self.__withdraw)
363 # more elaborate help is available in the README file
364 readmefile = os.path.join(sys.path[0], 'README')
365 try:
366 fp = None
367 try:
368 fp = open(readmefile)
369 contents = fp.read()
370 # wax the last page, it contains Emacs cruft
371 i = string.rfind(contents, '\f')
372 if i > 0:
373 contents = string.rstrip(contents[:i])
374 finally:
375 if fp:
376 fp.close()
377 except IOError:
378 sys.stderr.write("Couldn't open audiopy's README, "
379 'using docstring instead.\n')
380 contents = __doc__ % globals()
382 self.__text = text = Text(root, relief=SUNKEN,
383 width=80, height=24)
384 text.insert(0.0, contents)
385 scrollbar = Scrollbar(root)
386 scrollbar.pack(fill=Y, side=RIGHT)
387 text.pack(fill=BOTH, expand=YES)
388 text.configure(yscrollcommand=(scrollbar, 'set'))
389 scrollbar.configure(command=(text, 'yview'))
391 def __withdraw(self, event=None):
392 self.__root.withdraw()
394 def deiconify(self):
395 self.__root.deiconify()
400 def usage(code, msg=''):
401 print __doc__ % globals()
402 if msg:
403 print msg
404 sys.exit(code)
407 def main():
409 # Open up the audio control device and query for the current output
410 # device
411 device = sunaudiodev.open('control')
413 if len(sys.argv) == 1:
414 # GUI
415 w = MainWindow(device)
416 try:
417 w.start()
418 except KeyboardInterrupt:
419 pass
420 return
422 # spec: LONG OPT, SHORT OPT, 0=input,1=output, MASK
423 options = [('--microphone', '-m', 0, MICROPHONE),
424 ('--linein', '-i', 0, LINE_IN),
425 ('--headphones', '-p', 1, HEADPHONE),
426 ('--speaker', '-s', 1, SPEAKER),
427 ('--lineout', '-o', 1, LINE_OUT),
429 # See the comment above about `CD'
430 try:
431 options.append(('--cd', '-c', 0, CD))
432 except NameError:
433 pass
435 info = device.getinfo()
436 # first get the existing values
437 i = 0
438 while i < len(sys.argv)-1:
439 i = i + 1
440 arg = sys.argv[i]
441 if arg in ('-h', '--help'):
442 usage(0)
443 # does not return
444 elif arg in ('-g', '--gain'):
445 gainspec = '<missing>'
446 try:
447 gainspec = sys.argv[i+1]
448 gain = int(gainspec)
449 except (ValueError, IndexError):
450 usage(1, 'Bad gain specification: ' + gainspec)
451 info.o_gain = gain
452 i = i + 1
453 continue
454 elif arg in ('-v', '--version'):
455 print '''\
456 audiopy -- a program to control the Solaris audio device.
457 Contact: Barry Warsaw
458 Email: bwarsaw@python.org
459 Version: %s''' % __version__
460 sys.exit(0)
461 for long, short, io, mask in options:
462 if arg in (long, short):
463 # toggle the option
464 if io == 0:
465 info.i_port = info.i_port ^ mask
466 else:
467 info.o_port = info.o_port ^ mask
468 break
469 val = None
470 try:
471 if arg[:len(long)+1] == long+'=':
472 val = int(arg[len(long)+1:])
473 elif arg[:len(short)+1] == short+'=':
474 val = int(arg[len(short)+1:])
475 except ValueError:
476 usage(1, msg='Invalid option: ' + arg)
477 # does not return
478 if val == 0:
479 if io == 0:
480 info.i_port = info.i_port & ~mask
481 else:
482 info.o_port = info.o_port & ~mask
483 break
484 elif val == 1:
485 if io == 0:
486 info.i_port = info.i_port | mask
487 else:
488 info.o_port = info.o_port | mask
489 break
490 # else keep trying next option
491 else:
492 usage(1, msg='Invalid option: ' + arg)
493 # now set the values
494 try:
495 device.setinfo(info)
496 except sunaudiodev.error, (code, msg):
497 if code <> errno.EINVAL:
498 raise
499 device.close()
503 if __name__ == '__main__':
504 main()