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