1#! /usr/bin/env python 2 3"""audiopy -- a program to control the Solaris audio device. 4 5Contact: Barry Warsaw 6Email: bwarsaw@python.org 7Version: %(__version__)s 8 9When no arguments are given, this pops up a graphical window which lets you 10choose the audio input and output devices, and set the output volume. 11 12This program can be driven via the command line, and when done so, no window 13pops up. Most options have the general form: 14 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. 19 20The list of devices and their short options are: 21 22 (input) 23 microphone -- m 24 linein -- i 25 cd -- c 26 27 (output) 28 headphones -- p 29 speaker -- s 30 lineout -- o 31 32Other options are: 33 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] 38 39 --version 40 -v 41 Print the version number and exit. 42 43 --help 44 -h 45 Print this message and exit. 46""" 47 48import sys 49import os 50import errno 51import sunaudiodev 52from SUNAUDIODEV import * 53 54# Milliseconds between interrupt checks 55KEEPALIVE_TIMER = 500 56 57__version__ = '1.1' 58 59 60 61class MainWindow: 62 def __init__(self, device): 63 from Tkinter import * 64 self.__helpwin = None 65 self.__devctl = device 66 info = device.getinfo() 67 # 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) 96 # 97 buttons = [] 98 # 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() 105 ## 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) 118 ## 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) 131 ## 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 161 # 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) 180 ## 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) 194 ## 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) 208 # 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) 220 # 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) 235 # 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() 251 252 def __quit(self, event=None): 253 self.__devctl.close() 254 self.__root.quit() 255 256 def __popup_about(self, event=None): 257 import tkMessageBox 258 tkMessageBox.showinfo('About Audiopy ' + __version__, 259 '''\ 260Audiopy %s 261Control the Solaris audio device 262 263For information 264Contact: Barry A. Warsaw 265Email: bwarsaw@python.org''' % __version__) 266 267 def __popup_using(self, event=None): 268 if not self.__helpwin: 269 self.__helpwin = Helpwin(self.__tkroot, self.__quit) 270 self.__helpwin.deiconify() 271 272 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() 279 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) 302 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 315 316 def __getset(self, var, onvalue): 317 if var.get() == onvalue: 318 var.set(0) 319 else: 320 var.set(onvalue) 321 self.__pushtodev() 322 323 def __none(self, event=None): 324 self.__inputvar.set(0) 325 self.__pushtodev() 326 327 def __mic(self, event=None): 328 self.__getset(self.__inputvar, MICROPHONE) 329 330 def __linein(self, event=None): 331 self.__getset(self.__inputvar, LINE_IN) 332 333 def __cd(self, event=None): 334 self.__getset(self.__inputvar, CD) 335 336 def __speaker(self, event=None): 337 self.__getset(self.__spkvar, SPEAKER) 338 339 def __headphones(self, event=None): 340 self.__getset(self.__headvar, HEADPHONE) 341 342 def __lineout(self, event=None): 343 self.__getset(self.__linevar, LINE_OUT) 344 345 def __volume(self, event=None): 346 self.__pushtodev() 347 348 def start(self): 349 self.__keepalive() 350 self.__tkroot.mainloop() 351 352 353 354class 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) 365 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() 384 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')) 393 394 def __withdraw(self, event=None): 395 self.__root.withdraw() 396 397 def deiconify(self): 398 self.__root.deiconify() 399 400 401 402 403def usage(code, msg=''): 404 print __doc__ % globals() 405 if msg: 406 print msg 407 sys.exit(code) 408 409 410def main(): 411 # 412 # Open up the audio control device and query for the current output 413 # device 414 device = sunaudiodev.open('control') 415 416 if len(sys.argv) == 1: 417 # GUI 418 w = MainWindow(device) 419 try: 420 w.start() 421 except KeyboardInterrupt: 422 pass 423 return 424 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), 431 ] 432 # See the comment above about `CD' 433 try: 434 options.append(('--cd', '-c', 0, CD)) 435 except NameError: 436 pass 437 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 '''\ 459audiopy -- a program to control the Solaris audio device. 460Contact: Barry Warsaw 461Email: bwarsaw@python.org 462Version: %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() 503 504 505 506if __name__ == '__main__': 507 main() 508